Cross-Chain Bridge Security Post-2024 Hacks: LA Tech Week Lessons Learned

At LA Tech Week 2025’s “Bridge Security Summit,” the elephant in the room was impossible to ignore: $2.8 billion stolen from bridges in 2022-2024.

After analyzing every major hack, we’ve identified clear patterns. Here’s what we learned about building secure bridges.

The Bridge Hack Landscape (2022-2024)

Major Bridge Exploits

Ronin Bridge (March 2022): $625M stolen

  • Attack: Compromised 5 of 9 validator keys
  • Root cause: Centralized validator set, poor key management

Wormhole (February 2022): $325M stolen

  • Attack: Signature verification bypass
  • Root cause: Solana contract allowed spoofing guardian signatures

Nomad Bridge (August 2022): $190M stolen

  • Attack: Merkle root initialized to zero (trusted by default)
  • Root cause: Implementation bug in initialization

Harmony Bridge (June 2022): $100M stolen

  • Attack: Compromised 2 of 5 multisig keys
  • Root cause: Centralized multisig, poor operational security

BNB Bridge (October 2022): $586M stolen (mostly recovered)

  • Attack: Merkle proof forgery
  • Root cause: Improper proof verification

Multichain (July 2023): $126M stolen

  • Attack: CEO arrested, private keys seized by authorities
  • Root cause: Single point of failure (CEO controlled MPC keys)

Total losses (2022-2024): $2.8B+ across 20+ major exploits

Common Vulnerability Patterns

1. Centralized Validator Sets

The Problem: Most bridges use small validator sets (3-9 validators).

Attack surface: Compromise majority → drain bridge.

Examples:

  • Ronin: 5 of 9 validators (attacker got 5)
  • Harmony: 2 of 5 multisig (attacker got 2)

Why centralized?

  • Cheaper to run (fewer validators)
  • Faster finality (less coordination)
  • Easier to launch

The tradeoff: Security vs speed/cost.

2. Signature Verification Bugs

Pattern: Bridge doesn’t properly verify cross-chain messages.

Wormhole exploit code:

// VULNERABLE CODE (simplified)
function completeTransfer(bytes memory encodedVaa) public {
    // VAA = Verified Action Approval (guardian signature)
    (address[] memory signers, bytes memory payload) = parseVAA(encodedVaa);

    // BUG: Didn't verify signers were actual guardians
    // Attacker provided fake signers, minted tokens

    processTransfer(payload);
}

Fix:

function completeTransfer(bytes memory encodedVaa) public {
    (address[] memory signers, bytes memory payload) = parseVAA(encodedVaa);

    // CRITICAL: Verify signers are approved guardians
    require(areValidGuardians(signers), "Invalid guardians");
    require(signers.length >= quorum, "Insufficient signatures");

    processTransfer(payload);
}

function areValidGuardians(address[] memory signers) internal view returns (bool) {
    for (uint i = 0; i < signers.length; i++) {
        if (!approvedGuardians[signers[i]]) {
            return false;
        }
    }
    return true;
}

3. Merkle Proof Vulnerabilities

Nomad Bridge exploit:

// VULNERABLE: Merkle root initialized to 0x00
bytes32 public committedRoot = 0x0000...;

function process(bytes memory message, bytes32[] memory proof) public {
    bytes32 messageHash = keccak256(message);

    // BUG: When committedRoot = 0x00, any proof validates
    require(MerkleProof.verify(proof, committedRoot, messageHash), "Invalid proof");

    // Process message (mint tokens, etc.)
}

Attack: Attacker submitted arbitrary messages with dummy proofs. All validated (0x00 root).

Fix:

bytes32 public committedRoot;
bool public initialized;

function initialize(bytes32 _root) external onlyOwner {
    require(!initialized, "Already initialized");
    require(_root != bytes32(0), "Invalid root");
    committedRoot = _root;
    initialized = true;
}

function process(bytes memory message, bytes32[] memory proof) public {
    require(initialized, "Not initialized");
    require(committedRoot != bytes32(0), "Invalid state");

    // Verify proof...
}

Bridge Architecture Comparison

Option 1: External Validators (Multichain, Axelar)

How it works:

  1. User locks tokens on Chain A
  2. Validators observe lock event
  3. Validators sign attestation
  4. User submits attestation to Chain B
  5. Chain B verifies signatures, mints tokens

Security model: Trust majority of validators.

Architecture:

contract ExternalValidatorBridge {
    mapping(address => bool) public validators;
    uint256 public quorum; // e.g., 2/3 majority

    struct CrossChainMessage {
        uint256 sourceChain;
        address token;
        address recipient;
        uint256 amount;
        uint256 nonce;
    }

    function withdraw(
        CrossChainMessage memory message,
        bytes[] memory signatures
    ) external {
        bytes32 messageHash = hashMessage(message);

        // Verify quorum of validator signatures
        require(signatures.length >= quorum, "Insufficient signatures");

        uint256 validSignatures = 0;
        for (uint i = 0; i < signatures.length; i++) {
            address signer = messageHash.recover(signatures[i]);
            if (validators[signer]) {
                validSignatures++;
            }
        }

        require(validSignatures >= quorum, "Quorum not reached");

        // Process withdrawal
        _mint(message.recipient, message.amount);
    }
}

Pros: Fast, cheap (no light client overhead)
Cons: Trust assumption (validators can collude)

Option 2: Light Clients (LayerZero, zkBridge)

How it works:

  1. User locks tokens on Chain A
  2. Relayer submits Chain A block headers to Chain B
  3. Chain B verifies headers (light client)
  4. User submits Merkle proof of lock transaction
  5. Chain B verifies proof against verified header

Security model: Trustless (cryptographic verification of source chain state)

Architecture:

contract LightClientBridge {
    mapping(uint256 => bytes32) public blockHashes; // Chain A blocks

    function updateBlockHash(
        uint256 blockNumber,
        bytes32 blockHash,
        bytes memory proof // e.g., ZK proof of PoS consensus
    ) external {
        require(verifyConsensusProof(proof, blockHash), "Invalid proof");
        blockHashes[blockNumber] = blockHash;
    }

    function withdraw(
        uint256 blockNumber,
        bytes memory receiptProof,
        bytes memory receipt
    ) external {
        // Verify receipt is in verified block
        bytes32 blockHash = blockHashes[blockNumber];
        require(blockHash != bytes32(0), "Block not verified");

        bytes32 receiptRoot = getReceiptRoot(blockHash);
        require(
            MerkleProof.verify(receiptProof, receiptRoot, keccak256(receipt)),
            "Invalid receipt proof"
        );

        // Decode receipt, process withdrawal
        (address token, address recipient, uint256 amount) = decodeReceipt(receipt);
        _mint(recipient, amount);
    }
}

Pros: Trustless, no external validators
Cons: Expensive (storing block headers), complex

Option 3: Optimistic Bridges (Across, Connext)

How it works:

  1. Relayer posts claim: “Lock event happened on Chain A”
  2. Watchers have dispute period (e.g., 30 min) to challenge
  3. If no challenge, claim accepted
  4. If challenged, on-chain verification (expensive, rarely used)

Security model: 1-of-N honesty (one honest watcher protects system)

Architecture:

contract OptimisticBridge {
    struct Claim {
        bytes32 messageHash;
        address claimer;
        uint256 timestamp;
        bool disputed;
    }

    mapping(bytes32 => Claim) public claims;
    uint256 public disputePeriod = 30 minutes;

    function claim(CrossChainMessage memory message) external {
        bytes32 hash = hashMessage(message);
        claims[hash] = Claim({
            messageHash: hash,
            claimer: msg.sender,
            timestamp: block.timestamp,
            disputed: false
        });
    }

    function dispute(bytes32 claimHash, bytes memory fraud_proof) external {
        Claim storage claim = claims[claimHash];
        require(!claim.disputed, "Already disputed");
        require(block.timestamp < claim.timestamp + disputePeriod, "Dispute period ended");

        // Verify fraud proof (expensive on-chain check)
        require(verifyFraudProof(fraud_proof), "Invalid fraud proof");

        claim.disputed = true;
        // Slash claimer
    }

    function finalize(bytes32 claimHash, CrossChainMessage memory message) external {
        Claim storage claim = claims[claimHash];
        require(block.timestamp >= claim.timestamp + disputePeriod, "Dispute period active");
        require(!claim.disputed, "Claim disputed");

        // Process message
        _mint(message.recipient, message.amount);
    }
}

Pros: Fast (optimistic), cheap (no verification unless disputed)
Cons: Delay (dispute period), liveness assumption (watchers must be online)

LayerZero vs Wormhole Deep Dive

LayerZero Architecture

Components:

  • Relayer: Fetches block headers from source chain
  • Oracle: Independent entity verifying block headers
  • Endpoint: On-chain contract receiving messages

Security: Relayer + Oracle must collude to forge messages (2-of-2 trust).

Code flow:

// Chain A (source)
contract LayerZeroEndpoint {
    function send(
        uint16 dstChainId,
        bytes memory payload,
        address payable refundAddress
    ) external payable {
        bytes32 hash = keccak256(abi.encodePacked(payload, nonce++));
        emit Packet(dstChainId, payload, hash);

        // Relayer observes event, fetches block header
    }
}

// Chain B (destination)
contract LayerZeroEndpoint {
    function validateAndDeliver(
        uint16 srcChainId,
        bytes memory payload,
        bytes memory relayerProof,
        bytes memory oracleProof
    ) external {
        // Verify relayer provided correct block header
        bytes32 blockHash = verifyRelayerProof(relayerProof);

        // Verify oracle attested to same block header
        require(verifyOracleProof(oracleProof, blockHash), "Oracle mismatch");

        // Deliver payload to application
        ILayerZeroReceiver(app).lzReceive(srcChainId, payload);
    }
}

Upgrade path: LayerZero V2 adds DVNs (Decentralized Verification Networks) - choose your own security model.

Wormhole Architecture

Components:

  • Guardians: 19 validators running full nodes on all chains
  • Guardian Network: PoS-style consensus among guardians
  • Contracts: Verify guardian signatures

Security: 13 of 19 guardians must sign (byzantine fault tolerance).

Code flow:

// Chain A (source)
contract WormholeBridge {
    function publishMessage(bytes memory payload) public returns (uint64 sequence) {
        sequence = nextSequence++;
        emit LogMessagePublished(msg.sender, sequence, payload);

        // Guardians observe, sign VAA (Verified Action Approval)
    }
}

// Chain B (destination)
contract WormholeBridge {
    mapping(address => bool) public guardians; // 19 guardians
    uint256 public quorum = 13;

    function parseAndVerifyVAA(bytes memory vaa) public {
        (address[] memory signers, bytes memory payload) = parseVAA(vaa);

        // Count valid guardian signatures
        uint256 validSignatures = 0;
        for (uint i = 0; i < signers.length; i++) {
            if (guardians[signers[i]]) validSignatures++;
        }

        require(validSignatures >= quorum, "Insufficient guardians");

        // Process payload
        processMessage(payload);
    }
}

Post-hack improvements:

  • Upgraded to 19 guardians (from 13)
  • Added monitoring/alerting
  • Implemented per-chain rate limits

Security Best Practices (2025)

1. Rate Limits

Limit maximum value bridged per time window.

contract RateLimitedBridge {
    uint256 public maxPerHour = 10_000_000e6; // $10M USDC
    uint256 public currentHourStart;
    uint256 public currentHourVolume;

    function withdraw(uint256 amount) external {
        // Reset if new hour
        if (block.timestamp >= currentHourStart + 1 hours) {
            currentHourStart = block.timestamp;
            currentHourVolume = 0;
        }

        require(currentHourVolume + amount <= maxPerHour, "Rate limit");
        currentHourVolume += amount;

        // Process withdrawal
    }
}

2. Pausability

Emergency pause if anomaly detected.

contract PausableBridge is Pausable {
    address public guardian; // Can pause instantly

    function withdraw(...) external whenNotPaused {
        // Bridge logic
    }

    function emergencyPause() external {
        require(msg.sender == guardian, "Only guardian");
        _pause();
    }
}

3. Upgrade Timelocks

Delay before upgrades take effect (give users time to exit).

contract TimelockBridge {
    uint256 public constant TIMELOCK = 7 days;

    function upgradeImplementation(address newImpl) external onlyOwner {
        // Schedule upgrade
        pendingImplementation = newImpl;
        upgradeETA = block.timestamp + TIMELOCK;

        emit UpgradeScheduled(newImpl, upgradeETA);
    }
}

4. Multi-Sig Governance

Require multiple parties for critical actions.

Use Gnosis Safe or similar (3-of-5, 5-of-9, etc.)

My Questions for the Community

  1. Which bridge architecture do you trust most? External validators, light clients, or optimistic?

  2. Speed vs security: Would you accept 30-min delays for trustless bridging?

  3. Insurance: Should bridges offer hack insurance (like Nexus Mutual)?

  4. Future: Will intent-based systems (Across V3, UniswapX) replace traditional bridges?

Bridges are the most attacked infrastructure in crypto. Security must be the top priority.

Brian Zhang
Protocol Architect @ LayerZero


Resources:

Brian, excellent security analysis! Let me add the developer perspective. I’ve integrated LayerZero, Wormhole, and Axelar into production apps. Here’s what actually works.

Quick Integration: LayerZero OFT (Omnichain Fungible Token)

Goal: Deploy token that exists on multiple chains, bridge seamlessly.

LayerZero OFT Standard:

import "@layerzerolabs/solidity-examples/contracts/token/oft/v2/OFTV2.sol";

contract MyToken is OFTV2 {
    constructor(
        address _lzEndpoint
    ) OFTV2("MyToken", "MTK", 8, _lzEndpoint) {
        _mint(msg.sender, 1_000_000 * 10**8);
    }
}

// Deploy same contract on Ethereum, Arbitrum, Optimism, etc.
// Tokens automatically bridgeable via LayerZero

User sends tokens cross-chain:

import { ethers } from 'ethers';

const oft = new ethers.Contract(tokenAddress, OFT_ABI, signer);

// Send 100 tokens from Ethereum to Arbitrum
await oft.sendFrom(
  myAddress,                    // from
  42161,                        // Arbitrum chain ID
  recipientAddressBytes,        // to (bytes format)
  ethers.parseUnits('100', 8),  // amount
  myAddress,                    // refund address
  ethers.ZeroAddress,           // zro payment address
  "0x",                         // adapter params
  { value: ethers.parseEther('0.01') } // gas for destination
);

That’s it. No custom bridge contracts, all handled by LayerZero.

Wormhole Token Bridge

For existing tokens (can’t redeploy as OFT):

import { transferFromEth, redeemOnEth } from '@certusone/wormhole-sdk';

// 1. Lock tokens on Ethereum
const tx = await transferFromEth(
  provider,
  WORMHOLE_BRIDGE_ADDRESS,
  signer,
  tokenAddress,
  amount,
  CHAIN_ID_ARBITRUM,
  recipientAddress
);

// 2. Wait for guardians to sign (~15 seconds)
const signed = await getSignedVAA(
  WORMHOLE_RPC,
  tx.chainId,
  tx.emitterAddress,
  tx.sequence
);

// 3. Redeem on Arbitrum
await redeemOnArbitrum(
  arbitrumProvider,
  signer,
  WORMHOLE_BRIDGE_ARBITRUM,
  signed
);

My Recommendations

For new tokens: Use LayerZero OFT (simplest)
For existing tokens: Wormhole or Axelar (established)
For trustless: Wait for ZK light client bridges (2026+)

Bridge integration is straightforward with modern SDKs.

Chris Anderson
Full-Stack Crypto Developer

Brian, Chris - great technical coverage! Let me add the liquidity perspective. Bridge economics are often overlooked, but they’re critical for UX.

The Liquidity Problem

Lock-and-mint bridges (most bridges):

  • Lock 1000 USDC on Ethereum → Mint 1000 USDC on Arbitrum
  • Problem: Need deep liquidity on destination chain for instant withdrawals

Liquidity pool bridges (Across, Connext):

  • LPs provide liquidity on destination chain
  • Relayers instant-fill user orders
  • Relayers rebalance pools via slow bridge
  • Better UX: Instant withdrawals, no waiting

Across Protocol Economics

How it works:

  1. User wants to bridge 1000 USDC from Ethereum → Arbitrum
  2. Relayer instantly sends 1000 USDC to user on Arbitrum (from LP pool)
  3. User’s 1000 USDC locked on Ethereum goes to relayer (rebalances pool)
  4. Relayer earns fee (e.g., 0.1% = $1)

LP incentives: Earn fees from bridge volume.

Current APYs (Oct 2025):

  • USDC pool: 3.2% APY
  • ETH pool: 2.8% APY
  • USDT pool: 3.5% APY

Risk: Rebalancing risk (if Ethereum → Arbitrum heavily one-directional, pools drain on Arbitrum)

Future: Intent-Based Bridging

Upcoming: UniswapX, Across V3, 1inch Fusion

  • User signs “intent” (I want 1000 USDC on Arbitrum)
  • Solvers compete to fill order (best price wins)
  • Most efficient routing (bridge + DEX aggregation)

This is the future of cross-chain UX.

Diana Martinez
DeFi Protocol Engineer @ Uniswap Labs