julien/lovepublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/julien/love.git
git clone ssh://git@rickub.com/julien/love.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Verify wETH by bytecode and deploy at one address on every chain #4

Merged@julien wants to merge 5-weth-bytecode-verification into main
Merged as 2f6da5ed8829.
  • @julien opened this merge request · 3h ago

    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.sh reads script/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 EXTCODEHASH values across 27 chains. Six OP Stack chains share the 0x42..06 predeploy 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 that DELEGATECALL elsewhere, 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 and EXTCODEHASH there is not keccak256 of 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 its EXTCODEHASH is 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; MockWETH is now the "correct behaviour, wrong bytecode" rejection case.
    • multichain-check.sh proves 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 --check and forge lint are 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.

    edited

Sign in to comment.