Verify wETH by bytecode and deploy at one address on every chain #4
5-weth-bytecode-verification into main2f6da5ed8829.- @julien opened this merge request · 3h agoedited
closes #5
Replaces the trusted wETH constructor argument with a bytecode check, so a LOVE instance can only ever be bound to a reviewed wETH implementation, and lands at one address on every chain.
Survey first
script/weth-codehashes.shreadsscript/weth-chains.tsv, fetches the code at each chain's wETH and groups the chains by implementation. Running it on 27 chains answered the open question in the design note, and the answer changed the design:- 23 distinct
EXTCODEHASHvalues across 27 chains. Six OP Stack chains share the0x42..06predeploy address and produce four different hashes, because solc's trailing metadata blob embeds a hash of the source path and comments. - Restricted to genuine, non-proxy wrappers it is 5 implementations, 14 chains, 10 exact codehashes.
- 8 chains carry bridged ETH with no
deposit()/withdraw(uint256)at all. 4 more are proxies thatDELEGATECALLelsewhere, where a codehash commits to a forwarder rather than to behaviour — allowlisting one would reinstate exactly the upgradeable-backdoor risk the allowlist exists to exclude. zkSync Era is not EVM bytecode andEXTCODEHASHthere is notkeccak256of the runtime code.
Deployment is therefore blocked on those 12 chains, which is the intended outcome rather than a gap.
What changed
WETHRegistry— permissionless, ownerless, write-once, no constructor arguments.register()takes exactly 1 wei and accepts a candidate only if itsEXTCODEHASHis allowlisted and a deposit/withdraw round trip of that wei succeeds. The wei is kept rather than refunded, so a registrar with no payable fallback can still register.receive()reverts outside a probe.Love's constructor takes no arguments and reads wETH from the registry. Breaking change to the deployment interface and to the resulting address. The creation code is now byte-identical everywhere, which is what buys one address per chain.- Tests run against real WETH9 bytecode etched from Base and Ethereum. A mock cannot be registered without adding its codehash to the allowlist, which would mean testing a different allowlist from the one that ships;
MockWETHis now the "correct behaviour, wrong bytecode" rejection case. multichain-check.shproves the actual claim. It gives two anvil nodes different wETH implementations at different addresses and fails unless LOVE lands at one address — the old version gave both nodes the same wETH and was structurally incapable of failing for the right reason.
65 tests pass;
forge fmt --checkandforge lintare clean.Design decisions worth a look
The allowlist stores exact codehashes, not metadata-stripped ones. Stripping on-chain means hashing only the prefix the candidate's own trailing bytes point at, which accepts a known implementation plus arbitrary appended bytes; the safety of that rests on control flow never reaching them, which holds for every family here but would have to keep holding for every family added later. Five extra constants is the cheaper side of that trade. The stripped hash stays in the tooling, where it groups exact hashes into families for review.
Known gap. The gate proves a candidate is a reviewed wETH implementation, not that it is the wETH a chain's ecosystem uses. Anyone can deploy a fresh WETH9 copy — same bytecode, no liquidity — and register it first, and write-once means that chain is then stuck with it. Nothing is stealable, since it is real WETH9 and fully redeemable; the damage is a peg to a token nobody holds. A minimum-balance requirement was considered and rejected as a threshold that is wrong on small chains and affordable on large ones. Mitigation is to check
WETH()against the chain's canonical address, and to deploy under a fresh salt if a chain gets squatted.Out of scope, deliberately
The design note's multisig/timelock allowlist governance and its bonded provisional path for novel chains. Both reintroduce the admin key LOVE does not have: a multi-sig that can extend the list is a key, and a slashable bond needs an adjudicator. A compile-time allowlist gets the property that matters — no key can revoke an entry and strand a live deployment — for none of the cost.
- 23 distinct
Sign in to comment.