← 0xrivet

your bridge's security page is lying to you

2026-08-07 · bridge security · portland, or

"secured by 19 guardians." "protected by a 48h timelock." "governed by a multisig."

claims. not evidence.

$2.8B drained from bridges since 2022. every one had a security page. so i built a framework to check what's actually on-chain. every number here is reproducible with cast and an rpc endpoint. no trust required.


bridge trust score

five layers. each scored 0–4. composite score = minimum of all five. attackers target the weakest layer, not the strongest one.

layerprotectsfailed at
L1 verificationmessage authenticityWormhole $326M
L2 independencevalidator diversityRonin $625M
L3 upgradecontract mutationNomad $190M
L4 timelockreaction windowHarmony $100M
L5 circuit breakerdrain velocityBybit $1.46B

security is multiplicative. one zero makes the product zero.


L1: verification

how does the destination chain know a message is real?

0single signer
1multisig < 5
25–12 independent validators
313+ validators or multi-method ISM
4zk light client — no trusted third party

the thing nobody talks about: LayerZero V2 DVN config is per pathway. a bridge can run 2-of-3 DVN on ethereum→arbitrum and 1-of-1 on ethereum→some-smaller-chain. docs never disclose this at the pathway level.

# read DVN config for a specific oapp + destination
cast call <SEND_LIBRARY> \
  "getUlnConfig(address,uint32)((uint64,uint8,uint8,uint8,address[],address[]))" \
  <OAPP_ADDRESS> <DST_EID>
# requiredDVNCount=1, optionalDVNCount=0 → single point of failure

wormhole has 19 guardians, 13-of-19 quorum. sounds great. but in jan 2024, a researcher disclosed that the genesis guardian set — one single key — was never expired on wormchain. one key could bypass the entire quorum on that chain. the key was allegedly destroyed. "allegedly" is not a security model. fixed within 48h after disclosure, but existed for years.

L2: independence

ronin's 5-of-9 is the case study everyone cites but nobody fully explains. mechanism: 4 of 9 validators were sky mavis infrastructure. the 5th was a residual signing allowlist from november 2021 — never revoked. compromising one dev's laptop yielded 5 of 9 keys.

0all keys, one org
1< 3 independent orgs
23–7 orgs
38+ publicly identified orgs
4permissionless with slashing

on-chain heuristic worth running: check if validator addresses share a common funding source. same deployer → likely same org.

L3: upgrade

trace the full authority chain. most people stop at "it's a multisig." don't.

proxy → EIP-1967 admin slot → ProxyAdmin → owner()
  → timelock? → proposer role → multisig → threshold/owners
# who controls upgrades?
cast storage <PROXY> \
  0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103

# eoa or contract?
cast code <ADMIN> | head -c 10
# "0x" = EOA = score 0. anything else = dig deeper

# if multisig:
cast call <ADMIN> "getThreshold()(uint256)"
cast call <ADMIN> "getOwners()(address[])"
0EOA
1multisig ≤ 2 or timelock < 1h
2multisig 3+ with 1–24h timelock
3multisig 4+ with 24h+ timelock
4on-chain governance + 48h+, or immutable

L4: timelock

the number that matters isn't the delay. it's who can change the delay.

cast call <TIMELOCK> "getMinDelay()(uint256)"
# 86400 = 24h. good.

# but can the admin zero it out?
cast call <TIMELOCK> "hasRole(bytes32,address)(bool)" \
  $(cast keccak "TIMELOCK_ADMIN_ROLE") <ADMIN>
# true = the timelock is theater

a timelock whose delay can be zeroed without delay is equivalent to no timelock.

L5: circuit breaker

bybit lost $1.46B in one batch. no rate limit. no pause. nothing fired.

cast call <BRIDGE> "paused()(bool)" 2>/dev/null

most bridges score 0 here. it's the least deployed layer and the one that would've mattered most.


four patterns the docs won't show you

asymmetric pathway. strong on flagship routes, 1-of-1 on secondary chains. the attacker doesn't need your best-secured pathway. they'll use the worst one.

timelock illusion. 48h timelock exists. admin holds TIMELOCK_ADMIN_ROLE. admin calls updateDelay(0). effective timelock: zero.

orphaned guardian. validator set rotated on mainnet. old set never expired on alt-L1. one old key bypasses the new quorum. wormchain bug was exactly this — disclosed jan 2024, existed for years.

the safe blind spot. everyone checks threshold and owners. nobody checks modules.

# modules can exec transactions WITHOUT meeting threshold
cast call <SAFE> "getModules()(address[])"
# non-empty → investigate immediately

# guard (additional tx validation — 0x00 means no guard)
cast storage <SAFE> \
  0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8

bybit's safe had 3-of-N threshold. the attacker didn't break it — they used operation: 1 (delegatecall) in execTransaction. signers approved what looked like a normal transfer. they actually signed a full wallet takeover. the threshold held. it just didn't matter.


60-second audit

five commands. tells you more than the security page.

B="<BRIDGE_PROXY>"

# 1. upgrade admin
cast storage $B \
  0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103

# 2. eoa or contract?
cast code <ADMIN> | head -c 10

# 3. multisig threshold
cast call <ADMIN> "getThreshold()(uint256)" 2>/dev/null

# 4. timelock delay
cast call <ADMIN> "getMinDelay()(uint256)" 2>/dev/null

# 5. pausable?
cast call $B "paused()(bool)" 2>/dev/null

if the admin is an eoa, you already know more than the documentation told you.


scope

BTS measures operational security around contracts, not correctness of contracts. doesn't model economic security, liveness, or off-chain key management. measures what the chain can prove. a floor, not a ceiling.


if your bridge's security can't be verified on-chain, it isn't security. it's marketing.


bridge-security-toolkit automates these checks. PRs welcome.

written between mass transits and mass caffeine somewhere in the pnw. — 0xrivet