DEV Community

0xrivet
0xrivet

Posted on

Your Bridge's Security Page Is Lying to You

I spent the last few months pulling apart bridge security pages and comparing what they claim to what the contracts actually enforce on-chain. It's not pretty.

This post is the scoring framework I ended up building. Everything here you can verify yourself with cast and an RPC endpoint — no trust required.

(Originally published at https://0xrivet.xyz/bridge-trust-score)


Bridge Trust Score

Five layers, each scored 0-4. The composite score is the minimum — because attackers go for the weakest one, not the strongest.

Layer What it protects Where it failed
L1 Verification Message authenticity Wormhole $326M
L2 Independence Validator diversity Ronin $625M
L3 Upgrade Contract mutation Nomad $190M
L4 Timelock Reaction window Harmony $100M
L5 Circuit Breaker Drain velocity Bybit $1.46B

Security here is multiplicative. One zero makes the whole thing zero.


L1 — Verification

How does the destination chain know a message is actually real?

0 = single signer, 1 = multisig under 5, 2 = 5-12 independent validators, 3 = 13+ or multi-method ISM, 4 = ZK light client.

The thing nobody talks about — LayerZero V2 DVN configuration is per pathway. I've seen bridges running 2-of-3 DVN on their main Ethereum→Arbitrum route and 1-of-1 on a smaller chain. The docs never break this down at the pathway level. You have to check:


bash
cast call <SEND_LIBRARY> \
  "getUlnConfig(address,uint32)((uint64,uint8,uint8,uint8,address[],address[]))" \
  <OAPP_ADDRESS> <DST_EID>

If requiredDVNCount comes back as 1 with 0 optionals — that's a single point of failure right there.

Wormhole has 19 guardians, 13-of-19 quorum. Sounds solid. But in January 2024 a researcher found that the genesis guardian set — one key — was never expired on Wormchain. One key bypassing the entire quorum. It was allegedly destroyed before discovery. "Allegedly"
isn't a security model. They fixed it within 48h, but it had been sitting there for years.

L2 — Independence

Everyone cites Ronin's 5-of-9 but nobody explains the actual mechanism. Four of the nine validators were Sky Mavis infrastructure. The fifth was a signing allowlist from November
2021 that was created for a transaction surge, expired in December, and was never revoked. Lazarus compromised one developer laptop and got 5 of 9 keys.

L3 — Upgrade Authority

Most people check "is it a multisig?" and stop there. Don't.

You need to trace the whole chain:

Proxy → EIP-1967 admin slot → ProxyAdmin → owner()
  → Timelock? → proposer role → Multisig → threshold/owners

# Step 1: who controls upgrades?
cast storage <PROXY> \
  0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103

# Step 2: is it an EOA or contract?
cast code <ADMIN> | head -c 10
# "0x" means EOA. That's a score of 0 right there.

L4 — Timelock

Here's the thing — the delay value isn't what matters. What matters is who can change it.

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

# But wait — can the admin set it to zero?
cast call <TIMELOCK> "hasRole(bytes32,address)(bool)" \
  $(cast keccak "TIMELOCK_ADMIN_ROLE") <ADMIN>
# If true, your timelock is theater.

A timelock whose delay can be zeroed without its own delay is the same as no timelock.

L5 — Circuit Breaker

Bybit lost $1.46B in a single batch. No rate limit triggered. No pause. Nothing.

Most bridges I've checked score 0 here. It's the least deployed layer and probably the one that would've mattered most.

---
Patterns I Keep Seeing

Asymmetric pathways. The flagship route is well-secured. The secondary chain nobody audited runs 1-of-1 DVN. Guess which one the attacker uses.

Timelock illusion. There's a 48h timelock. The admin holds
TIMELOCK_ADMIN_ROLE. One call to updateDelay(0) and it's gone.

Orphaned guardians. Validator set gets rotated on mainnet. Old set never expires on some alt-L1. The Wormchain bug was exactly this.

The Safe blind spot. Threshold and owners get checked. Modules don't. A module can execute transactions without meeting the threshold at all.

cast call <SAFE> "getModules()(address[])"
# If this returns anything — investigate immediately.

---
The 60-Second Version

Five commands. Run these and you'll know more than the security page tells you.

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 — Can it be paused?
cast call $B "paused()(bool)" 2>/dev/null

If the admin is an EOA, you already know more than the docs told you.

---
The toolkit that automates these checks is open source:
bridge-security-toolkit.
PRs welcome.

— 0xrivet / 0xrivet.xyz
Enter fullscreen mode Exit fullscreen mode

Top comments (0)