DEV Community

ohmygod
ohmygod

Posted on

The CrossCurve Bridge Exploit: How a Missing Gateway Check Let Attackers Spoof Axelar Messages and Drain $3M

Cross-chain bridges remain the highest-value targets in DeFi. The February 2026 CrossCurve exploit is a masterclass in what happens when a bridge contract trusts its own validation logic more than the messaging protocol it depends on. One missing access control check. $3 million gone across multiple chains. Let's tear it apart.

What Is CrossCurve?

CrossCurve (formerly EYWA) is a cross-chain liquidity bridge connecting multiple EVM networks. Its architecture combines three components:

  1. Axelar-based ReceiverAxelar contracts — receive and validate inbound cross-chain messages
  2. Internal PortalV2 bridge contracts — hold liquidity and release tokens when authorized by the receiver
  3. Multi-validation layer — Axelar, LayerZero, and EYWA's own Oracle Network for redundant verification

The selling point was defense-in-depth: multiple validation layers meant a single point of failure shouldn't be catastrophic. The reality was different.

The Attack: Step by Step

On February 1, 2026, an attacker identified that CrossCurve's ReceiverAxelar contract exposed an expressExecute() function — designed for expedited cross-chain execution — that was publicly callable and lacked the critical gateway origin check.

Step 1: Spoof the Message Origin

The attacker generated a fresh commandId and supplied fake sourceChain and sourceAddress parameters to make the call appear to be a legitimate Axelar cross-chain message:

// What the attacker called — simplified
receiverAxelar.expressExecute(
    freshCommandId,       // Never-before-seen ID
    "arbitrum",           // Fake source chain
    fakeSourceAddress,    // Attacker-controlled "source"
    maliciousPayload      // Token release instruction
);
Enter fullscreen mode Exit fullscreen mode

Step 2: Craft the Malicious Payload

The ABI-encoded payload instructed the contract to mint/transfer approximately 999.8 million EYWA tokens to the attacker's wallet. The payload format matched what a legitimate Axelar message would produce — the contract couldn't tell the difference.

Step 3: Bypass "Validation"

Here's where the design failed. The contract's validation consisted of:

  1. commandId uniqueness check — Had this ID been used before? No (the attacker used a fresh one each time). ✅ Pass.
  2. Confirmation threshold — Set to 1, effectively disabling multi-guardian verification. ✅ Pass.

What was missing: The Axelar Gateway validation check — the one function call that verifies a message actually originated from Axelar's cross-chain messaging system.

// What SHOULD have been there (and wasn't):
require(
    axelarGateway.validateContractCall(
        commandId, sourceChain, sourceAddress, payloadHash
    ),
    "Not approved by gateway"
);
Enter fullscreen mode Exit fullscreen mode

Step 4: Multi-Chain Drainage

The attacker repeated this process across every chain CrossCurve supported — Arbitrum, Ethereum, and others. The PortalV2 contract's balance dropped from ~$3 million to near zero.

Post-exploit, the attacker:

  • Swapped stolen tokens to WETH via CoW Protocol on Arbitrum
  • Bridged funds to Ethereum via Across Protocol
  • Minted EYWA on Ethereum (though frozen CEX deposits limited liquidation there)

Root Cause: The One-Line Fix That Would Have Saved $3M

The vulnerability is embarrassingly simple. In Axelar's standard integration pattern, every receiver contract MUST validate that inbound messages passed through the Axelar Gateway:

// Standard Axelar receiver pattern
function execute(
    bytes32 commandId,
    string calldata sourceChain,
    string calldata sourceAddress,
    bytes calldata payload
) external {
    // THIS IS THE CRITICAL CHECK
    bytes32 payloadHash = keccak256(payload);
    require(
        gateway.validateContractCall(
            commandId, sourceChain, sourceAddress, payloadHash
        ),
        "Not approved by gateway"
    );

    // Only NOW process the payload
    _processPayload(payload);
}
Enter fullscreen mode Exit fullscreen mode

CrossCurve's expressExecute() omitted this check entirely. The only guard was commandId uniqueness — a trivially bypassable control since the attacker can generate unlimited fresh IDs.

Two compounding factors amplified the damage:

Factor Expected Actual
expressExecute() access control Restricted to authorized relayers public / no restriction
Confirmation threshold Multi-guardian quorum (e.g., 3-of-5) Set to 1 (single confirmation)

The Bridge Security Anti-Pattern

CrossCurve's failure illustrates a recurring pattern in bridge exploits:

Bridge Contract Security = Message Validation × Access Control × Quorum

If ANY factor = 0, the entire product = 0
Enter fullscreen mode Exit fullscreen mode

Compare with other bridge exploits:

Bridge Year Root Cause Loss
Ronin 2022 Compromised validator keys (5/9 quorum) $624M
Wormhole 2022 Signature verification bypass $326M
Nomad 2022 Trusted root set to 0x00 (any message valid) $190M
IoTeX 2026 Compromised private key on Ethereum validator $4.3M
CrossCurve 2026 Missing gateway validation on expressExecute $3M

The pattern is consistent: bridges fail at the validation boundary, not in the fund management logic. The code that moves tokens is usually fine. The code that decides whether to move tokens is where the bodies are buried.

Defensive Patterns for Bridge Developers

1. Never Skip Gateway Validation on Express Paths

The "express" execution path exists for speed, not for skipping security. Every code path that can trigger fund releases MUST go through the messaging protocol's validation:

// WRONG: Express path skips validation for speed
function expressExecute(...) external {
    require(!usedCommandIds[commandId], "Already used");
    _execute(payload); // No gateway check!
}

// RIGHT: Express path validates first, then executes faster
function expressExecute(...) external {
    require(!usedCommandIds[commandId], "Already used");
    require(
        gateway.validateContractCall(commandId, sourceChain, sourceAddress, keccak256(payload)),
        "Not approved by gateway"
    );
    usedCommandIds[commandId] = true;
    _execute(payload);
}
Enter fullscreen mode Exit fullscreen mode

2. Implement Defense-in-Depth That Actually Defends

CrossCurve advertised three validation layers (Axelar, LayerZero, EYWA Oracle). But if the first layer's check is missing, the others never get a chance to reject the message. Defense-in-depth means each layer independently validates, not that they're stacked sequentially with bypass potential.

// Defense-in-depth pattern for bridge receivers
function executeWithMultiValidation(...) external {
    // Layer 1: Protocol gateway validation
    require(gateway.validateContractCall(...), "Gateway rejected");

    // Layer 2: Source chain allowlist
    require(allowedSourceChains[sourceChain], "Chain not allowed");

    // Layer 3: Source address verification
    require(
        trustedRemotes[sourceChain] == sourceAddress,
        "Untrusted remote"
    );

    // Layer 4: Rate limiting
    require(
        rateLimiter.checkAndUpdate(token, amount),
        "Rate limit exceeded"
    );

    _execute(payload);
}
Enter fullscreen mode Exit fullscreen mode

3. Set Meaningful Quorum Thresholds

A confirmation threshold of 1 is functionally the same as no threshold. For multi-guardian systems:

  • Minimum viable quorum: 2-of-3 for small bridges
  • Production quorum: 5-of-9 or higher for bridges holding >$1M
  • Never: Set threshold to 1 in production

4. Restrict Express Execution to Known Relayers

Even with gateway validation, expressExecute() should be callable only by authorized relayer addresses:

modifier onlyRelayer() {
    require(authorizedRelayers[msg.sender], "Not an authorized relayer");
    _;
}

function expressExecute(...) external onlyRelayer {
    // validation + execution
}
Enter fullscreen mode Exit fullscreen mode

Audit Checklist for Cross-Chain Bridge Contracts

Use this checklist when auditing any bridge receiver contract:

  • [ ] Every execution path (standard + express) validates messages through the gateway
  • [ ] expressExecute() is not publicly callable without access controls
  • [ ] Source chain and source address are validated against an allowlist
  • [ ] Confirmation/quorum threshold is set to a meaningful value (>1)
  • [ ] Rate limiting exists on token releases per time window
  • [ ] commandId reuse prevention is a secondary check, not the primary security boundary
  • [ ] Emergency pause functionality exists and is tested
  • [ ] Token release amounts are bounded per transaction
  • [ ] Multi-guardian/multi-sig validation cannot be bypassed by any code path

Detection: Catching This On-Chain

If you're monitoring bridge contracts, flag these patterns:

# Pseudocode for bridge exploit detection
def detect_bridge_exploit(tx):
    # Flag 1: expressExecute called by non-relayer
    if tx.function == "expressExecute" and tx.sender not in KNOWN_RELAYERS:
        alert("Unauthorized expressExecute call", severity="CRITICAL")

    # Flag 2: Large token release without corresponding deposit
    if tx.releases_tokens and tx.amount > THRESHOLD:
        deposit = find_matching_deposit(tx.commandId, tx.sourceChain)
        if not deposit:
            alert("Token release without matching deposit", severity="CRITICAL")

    # Flag 3: Rapid sequential releases (multi-chain drain pattern)
    recent_releases = get_releases(window="10m")
    if len(recent_releases) > NORMAL_RATE * 3:
        alert("Anomalous release velocity", severity="HIGH")
Enter fullscreen mode Exit fullscreen mode

Timeline

Time Event
Feb 1, 2026 Attacker begins exploiting expressExecute() across multiple chains
Feb 1, 2026 PortalV2 balances drop from ~$3M to near zero
Feb 1, 2026 Attacker swaps stolen tokens → WETH via CoW Protocol (Arbitrum)
Feb 1, 2026 Funds bridged to Ethereum via Across Protocol
Feb 1, 2026 CrossCurve team detects incident, shuts down platform
Post-exploit CrossCurve offers 10% white-hat bounty for fund return
Post-exploit EYWA tokens on Ethereum frozen at CEX deposit addresses

Key Takeaway

The CrossCurve exploit wasn't sophisticated. It didn't require flash loans, oracle manipulation, or zero-day cryptographic breaks. It required calling a public function with fabricated parameters — something any Solidity developer could do in five minutes.

The lesson for every bridge team: your express execution path is your weakest link. If it exists for speed, it will be exploited for theft unless it carries the same validation guarantees as your standard path.

And for auditors: when reviewing bridge contracts, don't just verify that validation exists somewhere in the codebase. Verify that it exists on every single code path that can trigger fund movements. The CrossCurve team had gateway validation logic — they just forgot to call it in the one function that mattered most.


This analysis is part of the DeFi Security Research series. Follow for weekly deep-dives into smart contract vulnerabilities, audit techniques, and defense patterns.

Top comments (0)