DEV Community

ohmygod
ohmygod

Posted on • Originally published at dreamworksecurity.hashnode.dev

Anatomy of the CrossCurve Bridge Hack: How a Missing Access Control in Axelar's expressExecute Drained $3M

Cross-chain bridges remain DeFi's most dangerous attack surface. On February 1, 2026, CrossCurve (formerly EYWA) lost an estimated $3 million across Ethereum and Arbitrum — not because of a novel zero-day, but because of a textbook access control oversight in how they integrated with the Axelar General Message Passing (GMP) SDK.

This article breaks down exactly what went wrong, the vulnerable code path, the attacker's exploitation strategy, and what every protocol integrating cross-chain messaging should learn from this incident.


What Is CrossCurve?

CrossCurve is a cross-chain yield protocol built on Curve AMM that uses a Consensus Bridge — a multi-protocol validation system integrating Axelar, LayerZero, Router Protocol, Asterizm, and Chainlink CCIP. The idea: a transaction only completes if multiple independent data sources reach consensus. If data diverges, the transaction is rejected.

In theory, this is a robust design. In practice, a single integration blunder in the Axelar receiver contract bypassed the entire consensus model.

The Vulnerability: expressExecute() vs execute()

CrossCurve's ReceiverAxelar contract inherits from Axelar GMP SDK's AxelarExpressExecutable (v5.10). This SDK exposes two critical functions:

The Safe Path: execute()

function execute(
    bytes32 commandId,
    string calldata sourceChain,
    string calldata sourceAddress,
    bytes calldata payload
) external {
    // ✅ Gateway validation - only Axelar-approved messages pass
    if (!gateway.validateContractCall(commandId, sourceChain, sourceAddress, keccak256(payload)))
        revert NotApprovedByGateway();
    _execute(sourceChain, sourceAddress, payload);
}
Enter fullscreen mode Exit fullscreen mode

The execute() function includes a critical guard: gateway.validateContractCall(). This ensures that only messages approved by the Axelar signer set — via AxelarGateway.execute() → approveContractCall() — can trigger execution on the destination chain.

The Dangerous Path: expressExecute()

function expressExecute(
    bytes32 commandId,
    string calldata sourceChain,
    string calldata sourceAddress,
    bytes calldata payload
) external payable virtual {
    // ⚠️ Only checks if commandId was already used
    if (gateway.isCommandExecuted(commandId)) revert AlreadyExecuted();

    // ❌ NO gateway.validateContractCall() — anyone can call this
    address expressExecutor = msg.sender;
    // ... stores express executor, then calls _execute()
    _execute(sourceChain, sourceAddress, payload);
}
Enter fullscreen mode Exit fullscreen mode

expressExecute() is designed for "express relayers" who front-run cross-chain messages by executing them before Axelar finality, then get reimbursed when the real message arrives. But here's the critical design issue: it's a public function with no gateway validation.

The only check? Whether the commandId has been used before. Since commandId values aren't registered on the gateway until a real Axelar message is processed, the attacker simply fabricated a fresh commandId and bypassed everything.

CrossCurve never overrode expressExecute() to add access controls. They inherited it as-is from the SDK, leaving a wide-open backdoor.

The Attack Path

The attacker's exploitation was elegant in its simplicity:

Step 1: Craft the Malicious Call

ReceiverAxelar.expressExecute(
    commandId:     0x5e77d680...  // Fresh, unused — passes isCommandExecuted
    sourceChain:   "berachain"     // Legitimate peer registered on the contract
    sourceAddress: 0x5eEdDcE7...   // Legitimate peer address
    payload:       <malicious>     // Spoofed cross-chain unlock message
)
Enter fullscreen mode Exit fullscreen mode

Step 2: Pass the Peer Validation

Inside _execute(), the contract checks whether the sourceChain and sourceAddress are registered peers. The attacker queried these values directly from the contract's public storage — they were "berachain" and a specific address that anyone could look up on Etherscan.

Step 3: Spoof the Unlock Payload

The payload was crafted to instruct the Receiver contract (which consolidates all bridge data) to call PortalV2.unlock(), releasing tokens to the attacker's address. Since the payload came through a "trusted" code path (the ReceiverAxelar contract), PortalV2 had no reason to reject it.

Step 4: Repeat Across Chains

The attacker executed the same exploit on both Ethereum Mainnet and Arbitrum, draining tokens including EYWA, USDT0, and others — approximately $3 million total.

Why the Consensus Bridge Didn't Help

CrossCurve's multi-protocol consensus model was designed to prevent exactly this kind of attack. But expressExecute() bypassed the entire consensus flow:

  • It didn't go through AxelarGateway's approval process
  • It didn't trigger any of the other bridge validators
  • It went directly from external caller → ReceiverAxelar → Receiver → PortalV2 → token unlock

The consensus mechanism only protects the execute() path. The express path was a completely unguarded shortcut.

The Root Cause: Integration vs Protocol Security

This hack highlights a critical distinction that many DeFi teams miss. CrossCurve had a sound multi-bridge consensus design, but the Axelar SDK integration used default inheritance without proper overrides. The execute() path had gateway validation ✅, but expressExecute() had zero access control ❌.

Peer validation was present but insufficient, and the payload validation relied solely on the trusted path assumption with no independent verification.

Defensive Patterns: What Should Have Been Done

1. Override expressExecute() with Access Controls

function expressExecute(
    bytes32 commandId,
    string calldata sourceChain,
    string calldata sourceAddress,
    bytes calldata payload
) external payable override {
    // Only allow whitelisted express relayers
    require(isApprovedRelayer[msg.sender], "Unauthorized relayer");
    super.expressExecute(commandId, sourceChain, sourceAddress, payload);
}
Enter fullscreen mode Exit fullscreen mode

2. Or Disable It Entirely

function expressExecute(
    bytes32, string calldata, string calldata, bytes calldata
) external payable override {
    revert("Express execution disabled");
}
Enter fullscreen mode Exit fullscreen mode

If you're not using express relayers, don't expose the function. Dead code is safe code.

3. Independent Payload Verification

Even with access controls on the entry point, the unlock logic in PortalV2 should independently verify that the claimed cross-chain transfer actually occurred.

4. Rate Limiting and Anomaly Detection

Large unlocks should trigger circuit breakers with per-transaction and hourly limits.

The Broader Pattern: SDK Inheritance Is Not Security

This exploit belongs to a growing class of DeFi hacks where the vulnerability isn't in the protocol's own code, but in how it integrates third-party SDKs and frameworks:

  • Ronin Bridge (2022): Trusted external validators that were compromised
  • Wormhole (2022): Improper validation of guardian signatures
  • CrossCurve (2026): Unprotected inherited function from Axelar SDK

The lesson is clear: when you inherit from a third-party SDK, you inherit its entire attack surface. Every public and external function needs to be reviewed, and dangerous ones need to be overridden or disabled.

Security Checklist for Cross-Chain Integrations

  1. Audit every inherited function — not just the ones you call
  2. Override or disable express/fast-path functions unless actively used with proper access controls
  3. Never trust the message path alone — verify the message content independently
  4. Implement defense-in-depth: rate limits, pausing mechanisms, multi-sig for large withdrawals
  5. Test with adversarial scenarios: "What if someone calls expressExecute directly?"
  6. Monitor for unusual cross-chain messages using tools like Forta or custom alerting

Conclusion

The CrossCurve hack is a masterclass in why cross-chain bridge security is so unforgiving. A single unprotected function — inherited from a well-known SDK without modification — created a backdoor that bypassed an otherwise sophisticated multi-bridge consensus system.

For security researchers: this is a reminder to always check the full inheritance tree. The most dangerous vulnerabilities often hide in functions that protocol developers never explicitly wrote.

For protocol teams: integration is where security goes to die. Audit not just your code, but how your code meets the libraries, SDKs, and external systems it depends on.


DreamWork Security researches and publishes DeFi security analysis. Follow for vulnerability breakdowns, audit tool guides, and smart contract security best practices.

References:

Top comments (0)