Cross-chain bridges are critical infrastructure for blockchain interoperability, enabling asset transfers between different networks. Yet they represent the highest-risk component in DeFi, with over $2.5 billion stolen in bridge hacks since 2021. This article dissects bridge architectures, security mechanisms, and real-world vulnerabilities.
A cross-chain bridge allows assets to move between blockchains by locking assets on one chain and minting equivalent representations on another.
Core mechanisms:
The most common bridge design uses custody on the source chain and minting on the destination.
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.20;
3
4import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
6import "@openzeppelin/contracts/access/AccessControl.sol";
7
8contract BridgeLock is ReentrancyGuard, AccessControl {
9 bytes32 public constant VALIDATOR_ROLE = keccak256("VALIDATOR_ROLE");
10
11 // Minimum validators required for consensus
12 uint256 public requiredValidators;
13
14 // Track locked amounts
15 mapping(address => mapping(uint256 => uint256)) public lockedAmounts;
16
17 // Bridge nonce for unique transfers
18 uint256 public bridgeNonce;
19
20 // Validator approvals for each transfer
21 mapping(uint256 => mapping(address => bool)) public validatorApprovals;
22 mapping(uint256 => uint256) public approvalCount;
23
24 event TokensLocked(
25 uint256 indexed transferId,
26 address indexed token,
27 address indexed sender,
28 address recipient,
29 uint256 amount,
30 uint256 destinationChainId
31 );
32
33 event TransferApproved(
34 uint256 indexed transferId,
35 address indexed validator,
36 uint256 approvalCount
37 );
38
39 constructor(uint256 _requiredValidators) {
40 requiredValidators = _requiredValidators;
41 _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
42 }
43
44 /**
45 * @notice Lock tokens to bridge to another chain
46 * @param token Token contract address
47 * @param amount Amount to lock
48 * @param recipient Recipient address on destination chain
49 * @param destinationChainId Target chain ID
50 */
51 function lockTokens(
52 address token,
53 uint256 amount,
54 address recipient,
55 uint256 destinationChainId
56 ) external nonReentrant returns (uint256) {
57 require(amount > 0, "Amount must be > 0");
58 require(recipient != address(0), "Invalid recipient");
59
60 // Transfer tokens to this contract
61 IERC20(token).transferFrom(msg.sender, address(this), amount);
62
63 // Generate unique transfer ID
64 uint256 transferId = ++bridgeNonce;
65
66 // Record locked amount
67 lockedAmounts[token][transferId] = amount;
68
69 emit TokensLocked(
70 transferId,
71 token,
72 msg.sender,
73 recipient,
74 amount,
75 destinationChainId
76 );
77
78 return transferId;
79 }
80
81 /**
82 * @notice Validator approves a mint on destination chain
83 * @param transferId Transfer to approve
84 */
85 function approveTransfer(uint256 transferId)
86 external
87 onlyRole(VALIDATOR_ROLE)
88 {
89 require(!validatorApprovals[transferId][msg.sender], "Already approved");
90
91 validatorApprovals[transferId][msg.sender] = true;
92 approvalCount[transferId]++;
93
94 emit TransferApproved(
95 transferId,
96 msg.sender,
97 approvalCount[transferId]
98 );
99 }
100
101 /**
102 * @notice Unlock tokens (when burning happens on destination chain)
103 * @param transferId Original transfer ID
104 * @param token Token to unlock
105 * @param recipient Recipient of unlocked tokens
106 * @param amount Amount to unlock
107 */
108 function unlockTokens(
109 uint256 transferId,
110 address token,
111 address recipient,
112 uint256 amount
113 ) external nonReentrant onlyRole(VALIDATOR_ROLE) {
114 require(
115 approvalCount[transferId] >= requiredValidators,
116 "Insufficient approvals"
117 );
118 require(
119 lockedAmounts[token][transferId] >= amount,
120 "Insufficient locked amount"
121 );
122
123 // Update locked amount
124 lockedAmounts[token][transferId] -= amount;
125
126 // Transfer tokens
127 IERC20(token).transfer(recipient, amount);
128 }
129
130 /**
131 * @notice Add validator
132 */
133 function addValidator(address validator) external onlyRole(DEFAULT_ADMIN_ROLE) {
134 grantRole(VALIDATOR_ROLE, validator);
135 }
136
137 /**
138 * @notice Remove validator
139 */
140 function removeValidator(address validator) external onlyRole(DEFAULT_ADMIN_ROLE) {
141 revokeRole(VALIDATOR_ROLE, validator);
142 }
143}
1441// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.20;
3
4import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5import "@openzeppelin/contracts/access/AccessControl.sol";
6import "@openzeppelin/contracts/security/Pausable.sol";
7
8contract WrappedToken is ERC20, AccessControl, Pausable {
9 bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
10 bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
11
12 // Track mints by transfer ID to prevent replay
13 mapping(uint256 => bool) public processedTransfers;
14
15 // Validator approvals for minting
16 mapping(uint256 => mapping(address => bool)) public mintApprovals;
17 mapping(uint256 => uint256) public mintApprovalCount;
18
19 uint256 public requiredApprovals;
20
21 event TokensMinted(
22 uint256 indexed transferId,
23 address indexed recipient,
24 uint256 amount
25 );
26
27 event MintApproved(
28 uint256 indexed transferId,
29 address indexed validator
30 );
31
32 constructor(
33 string memory name,
34 string memory symbol,
35 uint256 _requiredApprovals
36 ) ERC20(name, symbol) {
37 _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
38 _grantRole(PAUSER_ROLE, msg.sender);
39 requiredApprovals = _requiredApprovals;
40 }
41
42 /**
43 * @notice Validator approves a mint operation
44 * @param transferId Transfer ID from source chain
45 * @param recipient Address to receive minted tokens
46 * @param amount Amount to mint
47 */
48 function approveMint(
49 uint256 transferId,
50 address recipient,
51 uint256 amount
52 ) external onlyRole(MINTER_ROLE) whenNotPaused {
53 require(!processedTransfers[transferId], "Transfer already processed");
54 require(!mintApprovals[transferId][msg.sender], "Already approved");
55
56 mintApprovals[transferId][msg.sender] = true;
57 mintApprovalCount[transferId]++;
58
59 emit MintApproved(transferId, msg.sender);
60
61 // If enough approvals, mint tokens
62 if (mintApprovalCount[transferId] >= requiredApprovals) {
63 processedTransfers[transferId] = true;
64 _mint(recipient, amount);
65
66 emit TokensMinted(transferId, recipient, amount);
67 }
68 }
69
70 /**
71 * @notice Burn wrapped tokens to unlock on source chain
72 * @param amount Amount to burn
73 */
74 function burn(uint256 amount) external whenNotPaused {
75 _burn(msg.sender, amount);
76 }
77
78 /**
79 * @notice Pause contract in emergency
80 */
81 function pause() external onlyRole(PAUSER_ROLE) {
82 _pause();
83 }
84
85 /**
86 * @notice Unpause contract
87 */
88 function unpause() external onlyRole(PAUSER_ROLE) {
89 _unpause();
90 }
91}
92Decentralized validator networks secure the bridge by requiring consensus.
1import { ethers } from 'ethers';
2import { EventEmitter } from 'events';
3
4interface BridgeTransfer {
5 transferId: number;
6 token: string;
7 sender: string;
8 recipient: string;
9 amount: bigint;
10 sourceChainId: number;
11 destinationChainId: number;
12 timestamp: number;
13}
14
15interface ValidatorConfig {
16 privateKey: string;
17 sourceRpc: string;
18 destRpc: string;
19 lockContractAddress: string;
20 mintContractAddress: string;
21 requiredConfirmations: number;
22}
23
24class BridgeValidator extends EventEmitter {
25 private sourceProvider: ethers.JsonRpcProvider;
26 private destProvider: ethers.JsonRpcProvider;
27 private sourceWallet: ethers.Wallet;
28 private destWallet: ethers.Wallet;
29
30 private lockContract: ethers.Contract;
31 private mintContract: ethers.Contract;
32
33 private processedTransfers: Set<number> = new Set();
34 private requiredConfirmations: number;
35
36 constructor(config: ValidatorConfig) {
37 super();
38
39 this.sourceProvider = new ethers.JsonRpcProvider(config.sourceRpc);
40 this.destProvider = new ethers.JsonRpcProvider(config.destRpc);
41
42 this.sourceWallet = new ethers.Wallet(config.privateKey, this.sourceProvider);
43 this.destWallet = new ethers.Wallet(config.privateKey, this.destProvider);
44
45 this.requiredConfirmations = config.requiredConfirmations;
46
47 // Initialize contracts
48 const lockAbi = [
49 'event TokensLocked(uint256 indexed transferId, address indexed token, address indexed sender, address recipient, uint256 amount, uint256 destinationChainId)',
50 'function approveTransfer(uint256 transferId) external'
51 ];
52
53 const mintAbi = [
54 'function approveMint(uint256 transferId, address recipient, uint256 amount) external'
55 ];
56
57 this.lockContract = new ethers.Contract(
58 config.lockContractAddress,
59 lockAbi,
60 this.sourceWallet
61 );
62
63 this.mintContract = new ethers.Contract(
64 config.mintContractAddress,
65 mintAbi,
66 this.destWallet
67 );
68 }
69
70 /**
71 * Start listening for bridge events
72 */
73 async start(): Promise<void> {
74 console.log('Bridge validator starting...');
75
76 // Listen for lock events
77 this.lockContract.on('TokensLocked', async (
78 transferId: bigint,
79 token: string,
80 sender: string,
81 recipient: string,
82 amount: bigint,
83 destinationChainId: bigint,
84 event: ethers.Log
85 ) => {
86 const transfer: BridgeTransfer = {
87 transferId: Number(transferId),
88 token,
89 sender,
90 recipient,
91 amount,
92 sourceChainId: (await this.sourceProvider.getNetwork()).chainId,
93 destinationChainId: Number(destinationChainId),
94 timestamp: Date.now()
95 };
96
97 await this.handleLockEvent(transfer, event);
98 });
99
100 console.log('Validator listening for events...');
101 }
102
103 /**
104 * Handle token lock event
105 */
106 private async handleLockEvent(
107 transfer: BridgeTransfer,
108 event: ethers.Log
109 ): Promise<void> {
110 const transferId = transfer.transferId;
111
112 // Skip if already processed
113 if (this.processedTransfers.has(transferId)) {
114 console.log(`Transfer ${transferId} already processed`);
115 return;
116 }
117
118 console.log(`\nNew lock event: Transfer #${transferId}`);
119 console.log(` Token: ${transfer.token}`);
120 console.log(` Amount: ${ethers.formatEther(transfer.amount)}`);
121 console.log(` Recipient: ${transfer.recipient}`);
122
123 try {
124 // Wait for confirmations
125 await this.waitForConfirmations(event.blockNumber);
126
127 // Validate transfer
128 const isValid = await this.validateTransfer(transfer);
129
130 if (!isValid) {
131 console.error(`Transfer ${transferId} validation failed`);
132 this.emit('validationFailed', transfer);
133 return;
134 }
135
136 // Approve on source chain
137 console.log(`Approving transfer ${transferId} on source chain...`);
138 const approveTx = await this.lockContract.approveTransfer(transferId);
139 await approveTx.wait();
140
141 // Approve mint on destination chain
142 console.log(`Approving mint ${transferId} on destination chain...`);
143 const mintTx = await this.mintContract.approveMint(
144 transferId,
145 transfer.recipient,
146 transfer.amount
147 );
148 await mintTx.wait();
149
150 this.processedTransfers.add(transferId);
151 this.emit('transferProcessed', transfer);
152
153 console.log(`✅ Transfer ${transferId} processed successfully`);
154
155 } catch (error) {
156 console.error(`Error processing transfer ${transferId}:`, error);
157 this.emit('processingError', { transfer, error });
158 }
159 }
160
161 /**
162 * Wait for block confirmations
163 */
164 private async waitForConfirmations(blockNumber: number): Promise<void> {
165 console.log(`Waiting for ${this.requiredConfirmations} confirmations...`);
166
167 while (true) {
168 const currentBlock = await this.sourceProvider.getBlockNumber();
169 const confirmations = currentBlock - blockNumber;
170
171 if (confirmations >= this.requiredConfirmations) {
172 console.log(`✅ ${confirmations} confirmations received`);
173 break;
174 }
175
176 await new Promise(resolve => setTimeout(resolve, 2000)); // Poll every 2s
177 }
178 }
179
180 /**
181 * Validate transfer integrity
182 */
183 private async validateTransfer(transfer: BridgeTransfer): Promise<boolean> {
184 // 1. Check token is whitelisted
185 const isWhitelisted = await this.isTokenWhitelisted(transfer.token);
186 if (!isWhitelisted) {
187 console.error(`Token ${transfer.token} not whitelisted`);
188 return false;
189 }
190
191 // 2. Check amount is within limits
192 const isWithinLimits = await this.checkTransferLimits(transfer);
193 if (!isWithinLimits) {
194 console.error(`Transfer amount exceeds limits`);
195 return false;
196 }
197
198 // 3. Verify lock transaction
199 const isLocked = await this.verifyLock(transfer);
200 if (!isLocked) {
201 console.error(`Lock verification failed`);
202 return false;
203 }
204
205 return true;
206 }
207
208 private async isTokenWhitelisted(token: string): Promise<boolean> {
209 // In production: check against whitelist contract
210 const whitelist = [
211 '0x...', // USDC
212 '0x...', // USDT
213 '0x...' // WETH
214 ];
215 return whitelist.includes(token.toLowerCase());
216 }
217
218 private async checkTransferLimits(transfer: BridgeTransfer): Promise<boolean> {
219 const maxTransfer = ethers.parseEther('1000000'); // $1M max
220 const minTransfer = ethers.parseEther('10'); // $10 min
221
222 return transfer.amount >= minTransfer && transfer.amount <= maxTransfer;
223 }
224
225 private async verifyLock(transfer: BridgeTransfer): Promise<boolean> {
226 // Verify tokens are actually locked in contract
227 const balance = await this.lockContract.lockedAmounts(
228 transfer.token,
229 transfer.transferId
230 );
231
232 return balance >= transfer.amount;
233 }
234
235 /**
236 * Stop validator
237 */
238 stop(): void {
239 this.lockContract.removeAllListeners();
240 console.log('Validator stopped');
241 }
242}
243
244// Example usage
245async function main() {
246 const config: ValidatorConfig = {
247 privateKey: process.env.VALIDATOR_PRIVATE_KEY!,
248 sourceRpc: 'https://eth-mainnet.g.alchemy.com/v2/YOUR-API-KEY',
249 destRpc: 'https://polygon-mainnet.g.alchemy.com/v2/YOUR-API-KEY',
250 lockContractAddress: '0x...', // Ethereum
251 mintContractAddress: '0x...', // Polygon
252 requiredConfirmations: 12
253 };
254
255 const validator = new BridgeValidator(config);
256
257 // Event handlers
258 validator.on('transferProcessed', (transfer) => {
259 console.log(`Transfer processed: ${transfer.transferId}`);
260 });
261
262 validator.on('validationFailed', (transfer) => {
263 console.error(`Validation failed: ${transfer.transferId}`);
264 });
265
266 await validator.start();
267}
2681contract BridgeRateLimiter {
2 // Per-token daily limits
3 mapping(address => uint256) public dailyLimits;
4
5 // Track daily volume
6 mapping(address => mapping(uint256 => uint256)) public dailyVolume;
7
8 uint256 constant SECONDS_PER_DAY = 86400;
9
10 event RateLimitExceeded(address token, uint256 attempted, uint256 limit);
11
12 function checkRateLimit(address token, uint256 amount) internal view returns (bool) {
13 uint256 currentDay = block.timestamp / SECONDS_PER_DAY;
14 uint256 todayVolume = dailyVolume[token][currentDay];
15 uint256 limit = dailyLimits[token];
16
17 if (limit == 0) return true; // No limit set
18
19 return todayVolume + amount <= limit;
20 }
21
22 function updateDailyVolume(address token, uint256 amount) internal {
23 uint256 currentDay = block.timestamp / SECONDS_PER_DAY;
24 dailyVolume[token][currentDay] += amount;
25
26 if (!checkRateLimit(token, 0)) {
27 emit RateLimitExceeded(token, dailyVolume[token][currentDay], dailyLimits[token]);
28 }
29 }
30
31 function setDailyLimit(address token, uint256 limit) external {
32 // Only admin
33 dailyLimits[token] = limit;
34 }
35}
361contract BridgeTimelock {
2 struct PendingTransfer {
3 address token;
4 address recipient;
5 uint256 amount;
6 uint256 unlockTime;
7 bool executed;
8 }
9
10 mapping(uint256 => PendingTransfer) public pendingTransfers;
11
12 uint256 public smallTransferLimit = 100000 * 1e18; // $100k
13 uint256 public timelockDuration = 24 hours;
14
15 event TransferQueued(uint256 indexed transferId, uint256 unlockTime);
16 event TransferExecuted(uint256 indexed transferId);
17 event TransferCancelled(uint256 indexed transferId);
18
19 function queueTransfer(
20 uint256 transferId,
21 address token,
22 address recipient,
23 uint256 amount
24 ) internal {
25 if (amount > smallTransferLimit) {
26 // Large transfer - require timelock
27 uint256 unlockTime = block.timestamp + timelockDuration;
28
29 pendingTransfers[transferId] = PendingTransfer({
30 token: token,
31 recipient: recipient,
32 amount: amount,
33 unlockTime: unlockTime,
34 executed: false
35 });
36
37 emit TransferQueued(transferId, unlockTime);
38 } else {
39 // Small transfer - execute immediately
40 _executeTransfer(transferId, token, recipient, amount);
41 }
42 }
43
44 function executeTimelocked(uint256 transferId) external {
45 PendingTransfer storage transfer = pendingTransfers[transferId];
46
47 require(!transfer.executed, "Already executed");
48 require(block.timestamp >= transfer.unlockTime, "Still locked");
49
50 transfer.executed = true;
51
52 _executeTransfer(
53 transferId,
54 transfer.token,
55 transfer.recipient,
56 transfer.amount
57 );
58
59 emit TransferExecuted(transferId);
60 }
61
62 function cancelTimelocked(uint256 transferId) external {
63 // Only admin in emergency
64 PendingTransfer storage transfer = pendingTransfers[transferId];
65 require(!transfer.executed, "Already executed");
66
67 transfer.executed = true;
68 emit TransferCancelled(transferId);
69 }
70
71 function _executeTransfer(
72 uint256 transferId,
73 address token,
74 address recipient,
75 uint256 amount
76 ) internal virtual {
77 // Implementation depends on bridge type
78 }
79}
801import { ethers } from 'ethers';
2import Prometheus from 'prom-client';
3
4class BridgeMonitor {
5 private lockContract: ethers.Contract;
6 private mintContract: ethers.Contract;
7
8 // Prometheus metrics
9 private transferCounter: Prometheus.Counter;
10 private transferVolume: Prometheus.Gauge;
11 private failureCounter: Prometheus.Counter;
12 private processingTime: Prometheus.Histogram;
13
14 constructor(
15 lockContract: ethers.Contract,
16 mintContract: ethers.Contract
17 ) {
18 this.lockContract = lockContract;
19 this.mintContract = mintContract;
20
21 // Initialize metrics
22 this.transferCounter = new Prometheus.Counter({
23 name: 'bridge_transfers_total',
24 help: 'Total number of bridge transfers',
25 labelNames: ['direction', 'token']
26 });
27
28 this.transferVolume = new Prometheus.Gauge({
29 name: 'bridge_volume_usd',
30 help: 'Total bridge volume in USD',
31 labelNames: ['token']
32 });
33
34 this.failureCounter = new Prometheus.Counter({
35 name: 'bridge_failures_total',
36 help: 'Total number of failed transfers',
37 labelNames: ['reason']
38 });
39
40 this.processingTime = new Prometheus.Histogram({
41 name: 'bridge_processing_seconds',
42 help: 'Transfer processing time',
43 buckets: [1, 5, 10, 30, 60, 300, 600]
44 });
45 }
46
47 async monitorLockEvents(): Promise<void> {
48 this.lockContract.on('TokensLocked', async (
49 transferId,
50 token,
51 sender,
52 recipient,
53 amount,
54 destinationChainId
55 ) => {
56 const startTime = Date.now();
57
58 // Update metrics
59 this.transferCounter.inc({ direction: 'outbound', token });
60
61 // Track volume (would need price oracle in production)
62 const usdValue = await this.getUSDValue(token, amount);
63 this.transferVolume.set({ token }, usdValue);
64
65 // Check for anomalies
66 await this.checkAnomalies(transferId, amount, token);
67
68 const processingTime = (Date.now() - startTime) / 1000;
69 this.processingTime.observe(processingTime);
70 });
71 }
72
73 async checkAnomalies(
74 transferId: number,
75 amount: bigint,
76 token: string
77 ): Promise<void> {
78 // 1. Check if amount is unusual
79 const avgTransfer = await this.getAverageTransferAmount(token);
80 const stdDev = await this.getStdDev(token);
81
82 if (Number(amount) > avgTransfer + 3 * stdDev) {
83 console.warn(`⚠️ Anomaly detected: Transfer ${transferId}`);
84 console.warn(` Amount: ${ethers.formatEther(amount)}`);
85 console.warn(` Average: ${avgTransfer}, StdDev: ${stdDev}`);
86
87 this.sendAlert({
88 type: 'anomaly',
89 transferId,
90 amount: amount.toString(),
91 token
92 });
93 }
94
95 // 2. Check for rapid-fire transfers (possible attack)
96 const recentTransfers = await this.getRecentTransferCount(10); // 10 seconds
97 if (recentTransfers > 10) {
98 console.warn(`⚠️ High transfer rate: ${recentTransfers} in 10s`);
99 this.sendAlert({
100 type: 'high_rate',
101 count: recentTransfers
102 });
103 }
104 }
105
106 private async getUSDValue(token: string, amount: bigint): Promise<number> {
107 // In production: use Chainlink price oracle
108 return 0;
109 }
110
111 private async getAverageTransferAmount(token: string): Promise<number> {
112 // Calculate from historical data
113 return 0;
114 }
115
116 private async getStdDev(token: string): Promise<number> {
117 // Calculate from historical data
118 return 0;
119 }
120
121 private async getRecentTransferCount(seconds: number): Promise<number> {
122 // Query recent transfers
123 return 0;
124 }
125
126 private sendAlert(alert: any): void {
127 // Send to PagerDuty, Slack, etc.
128 console.error('ALERT:', JSON.stringify(alert, null, 2));
129 }
130
131 /**
132 * Export metrics for Prometheus
133 */
134 getMetrics(): string {
135 return Prometheus.register.metrics();
136 }
137}
138Vulnerability: Compromised validator keys
Root cause:
1// Vulnerable: Centralized key management
2class InsecureValidatorSet {
3 private validators: string[] = [
4 '0xValidator1',
5 '0xValidator2',
6 // ... only 9 validators, threshold 5
7 ];
8
9 private threshold = 5; // Too low!
10
11 // Keys stored in single AWS account (compromised)
12}
13Fix: Decentralize validators, use hardware security modules (HSMs), increase threshold
Vulnerability: Privilege escalation in keeper contract
1// Vulnerable code (simplified)
2contract VulnerableKeeper {
3 address public keeper;
4
5 // Anyone can call this!
6 function changeKeeper(address newKeeper) public {
7 keeper = newKeeper;
8 }
9
10 function withdrawFunds(address to, uint256 amount) public {
11 require(msg.sender == keeper, "Not keeper");
12 // Transfer funds
13 }
14}
15
16// Attack:
17// 1. Call changeKeeper(attackerAddress)
18// 2. Call withdrawFunds to drain bridge
19Fix:
1contract SecureKeeper {
2 address public keeper;
3 address public admin;
4
5 modifier onlyAdmin() {
6 require(msg.sender == admin, "Not admin");
7 _;
8 }
9
10 modifier onlyKeeper() {
11 require(msg.sender == keeper, "Not keeper");
12 _;
13 }
14
15 function changeKeeper(address newKeeper) external onlyAdmin {
16 keeper = newKeeper;
17 }
18
19 function withdrawFunds(address to, uint256 amount) external onlyKeeper {
20 // Transfer funds
21 }
22}
23Vulnerability: Signature verification bypass
1// Vulnerable: Missing signature validation
2function completeTransfer(bytes memory encodedVm) public {
3 (VM memory vm, bool valid, string memory reason) = parseAndVerifyVM(encodedVm);
4
5 // BUG: Didn't check 'valid' flag!
6 // if (!valid) revert();
7
8 // Process transfer even if signature invalid
9 _mint(vm.recipient, vm.amount);
10}
11Fix:
1function completeTransfer(bytes memory encodedVm) public {
2 (VM memory vm, bool valid, string memory reason) = parseAndVerifyVM(encodedVm);
3
4 require(valid, reason); // ✅ Check signature validity
5
6 _mint(vm.recipient, vm.amount);
7}
81contract SecureMultisig {
2 address[] public signers;
3 uint256 public threshold;
4
5 mapping(bytes32 => mapping(address => bool)) public confirmations;
6 mapping(bytes32 => uint256) public confirmationCount;
7
8 function submitTransaction(
9 address to,
10 uint256 value,
11 bytes memory data
12 ) external returns (bytes32) {
13 bytes32 txHash = keccak256(abi.encodePacked(to, value, data, block.timestamp));
14
15 require(!confirmations[txHash][msg.sender], "Already confirmed");
16
17 confirmations[txHash][msg.sender] = true;
18 confirmationCount[txHash]++;
19
20 if (confirmationCount[txHash] >= threshold) {
21 _executeTransaction(to, value, data);
22 }
23
24 return txHash;
25 }
26
27 function _executeTransaction(
28 address to,
29 uint256 value,
30 bytes memory data
31 ) private {
32 (bool success, ) = to.call{value: value}(data);
33 require(success, "Transaction failed");
34 }
35}
361contract CircuitBreaker {
2 bool public paused;
3 uint256 public pausedUntil;
4
5 uint256 public hourlyLimit;
6 mapping(uint256 => uint256) public hourlyVolume;
7
8 event CircuitBreakerTripped(string reason);
9
10 modifier whenNotPaused() {
11 require(!paused || block.timestamp >= pausedUntil, "Circuit breaker active");
12 _;
13 }
14
15 function checkAndUpdateVolume(uint256 amount) internal {
16 uint256 currentHour = block.timestamp / 3600;
17 hourlyVolume[currentHour] += amount;
18
19 if (hourlyVolume[currentHour] > hourlyLimit) {
20 paused = true;
21 pausedUntil = block.timestamp + 1 hours;
22
23 emit CircuitBreakerTripped("Hourly limit exceeded");
24 }
25 }
26}
27Real-world bridge performance and security data:
Wormhole Bridge (Post-hack, secured):
1Daily Volume: $150M - $300M
2Validator Set: 19 guardians (13 threshold)
3Supported Chains: 20+
4Average Transfer Time: 5-15 minutes
5Fee: 0.1% - 0.3%
6
7Security Measures:
8 ✅ Multi-sig guardians
9 ✅ Bug bounty ($10M max)
10 ✅ Formal verification
11 ✅ Real-time monitoring
12 ✅ Circuit breakers
13 ✅ Rate limits
14
15Incidents Since Relaunch: 0
16Multichain Bridge:
1Daily Volume: $50M - $150M
2Validator Set: MPC-based (no threshold)
3Supported Chains: 80+
4Average Transfer Time: 10-30 minutes
5Fee: 0.1%
6
7Security Measures:
8 ✅ Secure MPC
9 ✅ Time delays
10 ✅ Rate limits
11 ❌ Centralized nodes (risk)
12
13Incidents: Multiple small hacks (< $1M total)
14Synapse Bridge:
1Daily Volume: $20M - $50M
2Validator Set: Optimistic verification
3Supported Chains: 18
4Average Transfer Time: 20-60 minutes
5Fee: 0.05% - 0.15%
6
7Security Measures:
8 ✅ Optimistic validation
9 ✅ Fraud proofs
10 ✅ Economic security
11 ✅ Gradual rollout
12
13Incidents: 0 major hacks
14Cross-chain bridges remain the highest-risk DeFi infrastructure:
Security Statistics (2021-2025):
Key Vulnerabilities:
Best Practices:
Future Directions:
Building secure bridges requires paranoid engineering: assume compromise at every layer, validate everything, and build in redundancy. The $2.5B in losses proves that bridge security cannot be an afterthought.
Technical Writer
NordVarg Team is a software engineer at NordVarg specializing in high-performance financial systems and type-safe programming.
Get weekly insights on building high-performance financial systems, latest industry trends, and expert tips delivered straight to your inbox.