Skip to main content
Sigvex

Bridge Security Vulnerabilities

Detects vulnerabilities in cross-chain bridge implementations including missing message verification, replay attack exposure, unauthorized relay functions, weak finality assumptions, and protocol-specific issues in a major cross-chain messaging protocol, Axelar, and Hyperlane integrations.

Bridge Security Vulnerabilities

Overview

Remediation Guide: How to Fix Bridge Security Vulnerabilities

The bridge security detector identifies vulnerabilities in cross-chain bridge and messaging protocol implementations. Cross-chain bridges are among the highest-risk components in the DeFi ecosystem because they hold locked assets on one chain while minting or unlocking equivalents on another — creating an asymmetric trust problem. A message verification failure on one chain can unlock all locked assets on the other without any corresponding burn.

Sigvex identifies the following vulnerability classes in bridge-related functions (functions whose names contain: execute, process, relay, deposit, withdraw, claim, receive, submit, proveAndProcess, proveAndExecute, proveMessage):

  1. Missing message verification: The bridge executes cross-chain messages without cryptographically verifying the signatures of the validator set or guardian network.
  2. Replay attack vulnerability: The bridge does not track which message IDs (nonces) have already been executed, allowing replayed messages to unlock assets multiple times.
  3. Unauthorized relay: The relay function that submits and executes cross-chain messages has no access control, allowing any address to call it with arbitrary payloads.
  4. Weak finality assumptions: The bridge executes messages as soon as a minimum number of block confirmations have elapsed on the source chain, without accounting for chain reorgs that can re-order or reverse source transactions.
  5. a major cross-chain messaging protocol: Missing trusted remote validation: The lzReceive() handler does not verify that the message originated from the registered trusted remote application on the source chain.
  6. Axelar: Missing gateway validation: The execute() callback does not call gateway.validateContractCall() to confirm the message was approved by the Axelar gateway.
  7. Hyperlane: Missing ISM validation: The message handler does not verify the Interchain Security Module (ISM) signature before processing the message.
  8. Centralized guardian set: The bridge relies on a small number of validators (fewer than a meaningful threshold) whose compromise directly allows unauthorized minting.

Why This Is an Issue

Cross-chain bridges have produced the three largest DeFi exploits in history:

  • Ronin Bridge (2022): $625M. Attackers compromised 5 of 9 validator private keys and forged signatures for withdrawal transactions. The centralized validator set was the single point of failure.
  • Wormhole (2022): $320M. A missing signature verification check in the guardian set validation allowed the attacker to submit a forged VAA (Verified Action Approval) that minted 120,000 wETH on Solana without any corresponding deposit on Ethereum.
  • Nomad Bridge (2022): $190M. A routine upgrade introduced a bug that set the trusted root to bytes32(0), causing all messages to pass verification trivially. Within hours, over 300 copycat transactions drained the bridge.

The common thread: the verify-before-execute invariant was broken, either by omission, by a logical flaw, or by compromise of the verification keys.

How to Resolve

// Before: Vulnerable — message executed without verification
contract VulnerableBridge {
    mapping(bytes32 => bool) public executedMessages;

    function executeMessage(
        bytes calldata message,
        bytes calldata /* signatures — not checked */
    ) external {
        bytes32 messageId = keccak256(message);
        // VULNERABLE: no signature verification
        // VULNERABLE: no replay protection check
        _execute(message);
    }
}

// After: Verify signatures and record execution
contract SecureBridge {
    mapping(bytes32 => bool) public executedMessages;
    address[] public validators;
    uint256 public constant THRESHOLD = 2; // 2-of-3 multisig minimum

    function executeMessage(
        bytes calldata message,
        bytes[] calldata signatures
    ) external {
        bytes32 messageId = keccak256(message);

        // 1. Replay protection: reject already-executed messages
        require(!executedMessages[messageId], "Message already executed");

        // 2. Signature verification: require threshold signatures from validator set
        uint256 validSigs;
        bytes32 ethSignedHash = MessageHashUtils.toEthSignedMessageHash(messageId);
        for (uint256 i = 0; i < signatures.length; i++) {
            address signer = ECDSA.recover(ethSignedHash, signatures[i]);
            if (_isValidator(signer)) {
                validSigs++;
            }
        }
        require(validSigs >= THRESHOLD, "Insufficient validator signatures");

        // 3. Mark as executed before any external call (CEI)
        executedMessages[messageId] = true;

        // 4. Execute
        _execute(message);
    }
}

Examples

Vulnerable Code (a major cross-chain messaging protocol)

import "@layerzerolabs/solidity-examples/contracts/lzApp/LzApp.sol";

contract VulnerableLzReceiver is LzApp {
    function lzReceive(
        uint16 _srcChainId,
        bytes memory _srcAddress,
        uint64 _nonce,
        bytes memory _payload
    ) public override {
        // VULNERABLE: no check that _srcAddress == trustedRemoteLookup[_srcChainId]
        // Any address on any chain can call lzEndpoint and trigger this handler
        _processPayload(_payload);
    }
}

Fixed Code (a major cross-chain messaging protocol)

import "@layerzerolabs/solidity-examples/contracts/lzApp/NonblockingLzApp.sol";

// NonblockingLzApp validates trusted remote before calling _nonblockingLzReceive
contract SecureLzReceiver is NonblockingLzApp {
    constructor(address _endpoint) LzApp(_endpoint) {}

    // Set trusted remote via: setTrustedRemote(srcChainId, abi.encodePacked(srcAddress, address(this)))
    function _nonblockingLzReceive(
        uint16 _srcChainId,
        bytes memory _srcAddress,
        uint64 _nonce,
        bytes memory _payload
    ) internal override {
        // NonblockingLzApp has already verified _srcAddress against trustedRemoteLookup
        // and catches reverts to prevent blocking future messages
        _processPayload(_payload);
    }
}

Vulnerable Code (Axelar)

import "@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol";

contract VulnerableAxelarReceiver is AxelarExecutable {
    function _execute(
        string calldata sourceChain,
        string calldata sourceAddress,
        bytes calldata payload
    ) internal override {
        // VULNERABLE: AxelarExecutable base calls gateway.validateContractCall()
        // but if the developer overrides incorrectly or calls a direct function,
        // the validation can be skipped. Ensure super._execute() is called or
        // gateway.validateContractCall() is explicitly invoked.
        _processPayload(payload);
        // Missing: gateway.validateContractCall(commandId, sourceChain, sourceAddress, keccak256(payload))
    }
}

Sample Sigvex Output

{
  "detector_id": "bridge-security",
  "severity": "critical",
  "confidence": 0.72,
  "description": "Function executeMessage() in bridge contract processes cross-chain messages without verifying validator signatures. The message ID is not checked against an executed-message registry, enabling replay attacks. Any address can call this function (no access control on the relay function). This pattern matches the Wormhole and Nomad bridge exploit classes.",
  "location": { "function": "executeMessage(bytes,bytes)", "offset": 8 }
}

Detection Methodology

Sigvex identifies bridge security vulnerabilities through multi-pattern analysis:

  1. Bridge function identification: Matches function names against bridge-specific patterns. For layerzero/axelar/hyperlane, additionally scans for protocol-specific 4-byte selectors.
  2. Signature verification check: Verifies that bridge message processing functions include ECDSA signature recovery calls (ecrecover or equivalent library) and compare recovered addresses against a stored validator set. Missing signature verification → MissingVerification finding.
  3. Replay protection check: Searches for mapping-based nonce or message-ID tracking (storage write with message hash key) before or immediately after the execution path. Missing storage write → ReplayVulnerability finding.
  4. Access control on relay: Checks whether bridge relay functions include a caller check (require(msg.sender == relayer), onlyRole(RELAYER_ROLE), or equivalent). Missing check → UnauthorizedRelay finding.
  5. Protocol-specific checks: For a major cross-chain messaging protocol, checks that _srcAddress is compared against trustedRemoteLookup. For Axelar, checks that gateway.validateContractCall() is invoked. For Hyperlane, checks that ISM address is read from storage and a verify() call is present.
  6. Centralized guardian set: Detects validator set size from constructor or storage patterns. Guardian sets below a configurable threshold flag CentralizedGuardianSet.

Limitations

False positives:

  • Bridge contracts that delegate signature verification to a separate Verifier contract via an external call may be flagged because the external call pattern does not match the inline ecrecover signature.
  • a major cross-chain messaging protocol contracts that use the standard LzApp base class (which validates trusted remote internally) may be flagged if the base class is not identified from bytecode alone.

False negatives:

  • Cross-chain replay attacks that exploit different chain IDs (same message valid on multiple chains) require cross-chain analysis not available in single-contract mode.
  • Validator key compromise attacks are outside static analysis scope.

References