The CrossCurve $3M Bridge Exploit
How a single missing access control check let an attacker forge cross-chain messages — and the 4-layer defense every bridge must implement
On February 1, 2026, the CrossCurve bridge (formerly EYWA) lost approximately $3 million across Arbitrum, Ethereum, and several other chains. The root cause wasn't a sophisticated flash loan or a novel cryptographic attack — it was a publicly callable function with insufficient input validation.
This is one of those exploits that makes auditors wince, because the fix is obvious in hindsight. But it reveals a systemic problem in cross-chain bridge architecture that goes far beyond one protocol.
Background: How CrossCurve's Bridge Worked
CrossCurve used Axelar's General Message Passing (GMP) to relay cross-chain messages. The architecture looked like this:
Source Chain Destination Chain
┌─────────┐ Axelar GMP ┌──────────────────┐
│ Portal │ ──────────────► │ ReceiverAxelar │
│ (lock) │ commandId + │ (validate + exec) │
│ │ payload │ │ │
└─────────┘ │ ▼ │
│ PortalV2 │
│ (unlock/mint) │
└──────────────────┘
When a user deposited tokens on the source chain, the Portal contract locked them and emitted a message through Axelar. On the destination chain, ReceiverAxelar validated the message and instructed PortalV2 to release the corresponding tokens.
The critical assumption: only legitimate Axelar messages should trigger token releases.
The Vulnerability: expressExecute() Was Wide Open
The ReceiverAxelar contract exposed an expressExecute() function designed for fast-path cross-chain execution. Here's the simplified vulnerable logic:
// VULNERABLE — simplified from CrossCurve's ReceiverAxelar
function expressExecute(
bytes32 commandId,
string calldata sourceChain,
string calldata sourceAddress,
bytes calldata payload
) external {
// Only check: has this commandId been used before?
require(!executedCommands[commandId], "Already executed");
executedCommands[commandId] = true;
// ⚠️ NO validation of sourceChain authenticity
// ⚠️ NO validation of sourceAddress against whitelist
// ⚠️ NO multi-guardian confirmation requirement
// ⚠️ NO verification that Axelar actually relayed this message
// Decode and execute the payload
_executePayload(sourceChain, sourceAddress, payload);
}
Three critical flaws:
-
No source authentication — The function accepted any
sourceChainandsourceAddresswithout verifying they came from a trusted Axelar relayer - Confirmation threshold of 1 — Multi-guardian verification was effectively disabled
-
Publicly callable — Anyone could call
expressExecute()with fabricated parameters
The Attack: Step by Step
Step 1: Craft the Fake Message
The attacker generated a fresh commandId (any unique bytes32 value) and constructed a malicious payload:
// Attacker's fabricated cross-chain "message"
bytes32 commandId = keccak256(abi.encode(block.timestamp, attackerNonce));
string memory sourceChain = "ethereum"; // Spoofed
string memory sourceAddress = "0x<portal_address>"; // Spoofed
bytes memory payload = abi.encode(
UNLOCK_SELECTOR,
attackerAddress,
tokenAddress,
drainAmount // e.g., 1,000,000 USDC
);
Step 2: Call expressExecute()
// Direct call — no Axelar involvement needed
receiverAxelar.expressExecute(
commandId,
sourceChain,
sourceAddress,
payload
);
Step 3: Tokens Released
The ReceiverAxelar contract:
- ✅ Checked
commandIduniqueness — passed (fresh ID) - ❌ Did NOT verify the message came from Axelar
- ❌ Did NOT validate
sourceAddressagainst a whitelist - Called
PortalV2.unlock()→ tokens sent to attacker
Step 4: Repeat Across Chains
The attacker replicated this on Arbitrum, Ethereum, and other supported chains, draining approximately $3M total before CrossCurve's team detected the anomaly and shut down the platform.
Why This Vulnerability Class Keeps Recurring
CrossCurve isn't the first bridge to fall to message forgery. This is the same fundamental pattern that hit:
| Bridge | Year | Loss | Root Cause |
|---|---|---|---|
| Ronin (Axie) | 2022 | $625M | Compromised validator keys |
| Wormhole | 2022 | $325M | Signature verification bypass |
| Nomad | 2022 | $190M | Initialized with trusted root = 0x00 |
| CrossCurve | 2026 | $3M | Missing source authentication |
The common thread: bridges must solve the \"who sent this message?\" problem, and every shortcut in that verification is a potential $M+ vulnerability.
The 4-Layer Bridge Defense Model
After analyzing every major bridge exploit since 2022, here's the defense stack that actually works:
Layer 1: Source Authentication
// SECURE: Verify the message came from the actual relay network
function expressExecute(
bytes32 commandId,
string calldata sourceChain,
string calldata sourceAddress,
bytes calldata payload
) external {
// Verify caller is the authorized Axelar gateway
require(
msg.sender == axelarGateway,
"Unauthorized: not gateway"
);
// Verify the command was actually approved by Axelar validators
require(
IAxelarGateway(axelarGateway).validateContractCall(
commandId, sourceChain, sourceAddress, keccak256(payload)
),
"Not approved by gateway"
);
// ... rest of execution
}
Layer 2: Source Address Whitelisting
mapping(string => mapping(string => bool)) public trustedSources;
modifier onlyTrustedSource(
string calldata sourceChain,
string calldata sourceAddress
) {
require(
trustedSources[sourceChain][sourceAddress],
"Untrusted source"
);
_;
}
Layer 3: Rate Limiting and Caps
struct RateLimit {
uint256 maxPerTx;
uint256 maxPerHour;
uint256 usedThisHour;
uint256 hourStart;
}
function _checkRateLimit(address token, uint256 amount) internal {
RateLimit storage limit = rateLimits[token];
require(amount <= limit.maxPerTx, "Exceeds per-tx limit");
if (block.timestamp > limit.hourStart + 1 hours) {
limit.usedThisHour = 0;
limit.hourStart = block.timestamp;
}
limit.usedThisHour += amount;
require(
limit.usedThisHour <= limit.maxPerHour,
"Hourly limit exceeded"
);
}
Layer 4: Time-Delayed Execution for Large Transfers
uint256 public constant LARGE_TRANSFER_THRESHOLD = 500_000e6; // $500K
uint256 public constant DELAY_PERIOD = 30 minutes;
mapping(bytes32 => uint256) public pendingExecutions;
function _executeOrQueue(
bytes32 commandId,
bytes calldata payload,
uint256 amount
) internal {
if (amount > LARGE_TRANSFER_THRESHOLD) {
pendingExecutions[commandId] = block.timestamp + DELAY_PERIOD;
emit LargeTransferQueued(commandId, amount);
// Guardians can cancel during delay window
} else {
_execute(payload);
}
}
Bridge Security Audit Checklist
If you're auditing a bridge or building one, verify each item:
Message Validation:
- [ ]
expressExecute/ fast-path functions verify caller is the authorized gateway - [ ] Gateway's
validateContractCallis invoked before payload execution - [ ] Source chain and source address are checked against a maintained whitelist
- [ ]
commandIdreplay protection covers ALL execution paths (express + normal)
Access Control:
- [ ] Multi-guardian confirmation threshold > 1 (ideally 2/3+ of guardian set)
- [ ] Guardian rotation doesn't leave windows where threshold drops to 1
- [ ] Admin functions (whitelist updates, threshold changes) are behind timelock + multisig
Economic Controls:
- [ ] Per-transaction transfer caps are enforced at the contract level
- [ ] Hourly/daily volume limits exist and are monitored
- [ ] Large transfers trigger a time delay with guardian cancellation capability
- [ ] Circuit breaker exists: auto-pause if drain rate exceeds threshold
Monitoring:
- [ ] Real-time alerting on transfers > X% of TVL
- [ ] Anomaly detection on
expressExecutecall frequency - [ ] Cross-chain balance reconciliation runs continuously
- [ ] Incident response playbook includes contract pause procedure
Key Takeaways
Bridge fast-paths are attack magnets. Every
expressorfastfunction needs more validation, not less. Speed should never come at the cost of authentication.The \"who sent this?\" question is existential for bridges. If your bridge can't cryptographically prove that a cross-chain message originated from a trusted source, your TVL is a bounty.
Confirmation threshold = 1 is equivalent to no confirmation. If a single entity (or a publicly callable function) can authorize releases, you've created a single point of failure worth your entire TVL.
Defense in depth saves millions. Even if CrossCurve's source authentication had been bypassed, rate limits + time delays + monitoring could have limited the damage to thousands instead of millions.
The CrossCurve exploit is a $3M lesson in bridge security fundamentals. The vulnerability wasn't novel — it was a missing require statement that every bridge security guide warns about. The question for every bridge team: have you actually verified your fast-path functions, or are you assuming the happy path is the only path?
This is part of the DeFi Security Research series analyzing real-world exploits and the defense patterns that prevent them. Follow for weekly deep-dives into smart contract, bridge, and protocol security.
Top comments (0)