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:
- User locks tokens on Chain A
- Validators observe lock event
- Validators sign attestation
- User submits attestation to Chain B
- 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:
- User locks tokens on Chain A
- Relayer submits Chain A block headers to Chain B
- Chain B verifies headers (light client)
- User submits Merkle proof of lock transaction
- 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:
- Relayer posts claim: “Lock event happened on Chain A”
- Watchers have dispute period (e.g., 30 min) to challenge
- If no challenge, claim accepted
- 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
-
Which bridge architecture do you trust most? External validators, light clients, or optimistic?
-
Speed vs security: Would you accept 30-min delays for trustless bridging?
-
Insurance: Should bridges offer hack insurance (like Nexus Mutual)?
-
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:
- LayerZero: https://layerzero.network/
- Wormhole: https://wormhole.com/
- Rekt.news bridge hacks: Rekt - <!-- -->Leaderboard
- L2Beat bridge risk analysis: https://l2beat.com/bridges/risk
- LA Tech Week 2025 (October 13-19, Los Angeles)