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 LayerZero, 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):
- Missing message verification: The bridge executes cross-chain messages without cryptographically verifying the signatures of the validator set or guardian network.
- Replay attack vulnerability: The bridge does not track which message IDs (nonces) have already been executed, allowing replayed messages to unlock assets multiple times.
- 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.
- 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.
- LayerZero: Missing trusted remote validation: The
lzReceive()handler does not verify that the message originated from the registered trusted remote application on the source chain. - Axelar: Missing gateway validation: The
execute()callback does not callgateway.validateContractCall()to confirm the message was approved by the Axelar gateway. - Hyperlane: Missing ISM validation: The message handler does not verify the Interchain Security Module (ISM) signature before processing the message.
- 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 (LayerZero)
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 (LayerZero)
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": "high",
"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:
- Bridge function identification: Matches function names against bridge-specific patterns. For layerzero/axelar/hyperlane, additionally scans for protocol-specific 4-byte selectors.
- Signature verification check: Verifies that bridge message processing functions include ECDSA signature recovery calls (
ecrecoveror equivalent library) and compare recovered addresses against a stored validator set. Missing signature verification →MissingVerificationfinding. - 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 →
ReplayVulnerabilityfinding. - Access control on relay: Checks whether bridge relay functions include a caller check (
require(msg.sender == relayer),onlyRole(RELAYER_ROLE), or equivalent). Missing check →UnauthorizedRelayfinding. - Protocol-specific checks: For LayerZero, checks that
_srcAddressis compared againsttrustedRemoteLookup. For Axelar, checks thatgateway.validateContractCall()is invoked. For Hyperlane, checks that ISM address is read from storage and averify()call is present. - 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
Verifiercontract via an external call may be flagged because the external call pattern does not match the inlineecrecoversignature. - LayerZero contracts that use the standard
LzAppbase 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.
Related Detectors
- Access Control — unauthorized relay is an access control failure
- Signature Replay — replay protection requirements
- Oracle Manipulation — bridge oracle feeds are also manipulation targets