// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; import {IWETH} from "./IWETH.sol"; /// @title WETHRegistry /// @author Julien Béranger /// @notice Names the wETH that `Love` pegs to on this chain, and will only /// accept an address whose code is one of a fixed set of reviewed wETH /// implementations. /// @dev The point is to take the wETH address out of `Love`'s creation code. /// Anyone can call `register`, on any chain, without a factory, an owner /// or a per-chain deployer — the contract decides for itself whether to /// accept the candidate. Because it takes no constructor arguments, its /// own creation code is identical everywhere, so CREATE2 puts it at one /// address on every chain and `Love` can hardcode that address. /// /// Two gates run at registration: /// /// 1. `EXTCODEHASH` against the allowlist below. This is the real /// defence: matching means the candidate is provably one of a handful /// of implementations whose source has been read, not merely something /// that behaves well today. It is what catches a hidden mint, an /// upgrade hook or a backdoor, none of which a behavioural test can /// see. /// 2. A one-wei deposit/withdraw round trip, which catches the plain /// mistake of an address that is not wETH at all, and proves the /// wrapper actually works on this chain. /// /// The registration is write-once. There is no setter, no owner and no /// way to revoke an entry, so a `Love` deployed against this registry can /// never have the token under its peg swapped. /// /// Known limitation, and it is not a small one: the allowlist proves a /// candidate *is* a reviewed wETH implementation, not that it is *the* /// wETH the chain's ecosystem uses. Anyone can deploy their own copy of /// WETH9 — identical code, identical codehash, no liquidity — and /// register it first. The result is still fully backed and redeemable, /// since it is real WETH9, but it is not the token anyone else holds, and /// write-once means the mistake cannot be corrected on that chain. So /// always check `weth()` against the chain's canonical wETH before /// treating a `Love` instance as the real one; a squatted registry is /// visible to anyone who looks, and the answer to it is a fresh salt. contract WETHRegistry { /// @notice The wETH registered on this chain, or the zero address if none /// has been registered yet. /// @dev Written exactly once, by whoever calls `register` first with a /// candidate that passes both gates. IWETH public weth; /// @notice The ether moved through the candidate to prove it wraps. /// @dev Stays in the registry afterwards; see `register`. uint256 public constant PROBE = 1 wei; /// @dev Open only for the duration of the round trip, so the registry /// cannot be used as a place to park ether. Transient, so it costs /// almost nothing and cannot survive the call that set it. bool private transient _probing; /// @notice Thrown when a wETH has already been registered on this chain. /// @param registered The wETH registered by the earlier call. error AlreadyRegistered(IWETH registered); /// @notice Thrown when the candidate's code is not a reviewed wETH. /// @param candidate The rejected address. /// @param codeHash Its `EXTCODEHASH`, zero if there is no code there. error UnknownImplementation(IWETH candidate, bytes32 codeHash); /// @notice Thrown when the call does not carry exactly `PROBE` wei. /// @param sent The value that came with the call. error ProbeValueRequired(uint256 sent); /// @notice Thrown when wrapping `PROBE` wei did not mint `PROBE` wrapped. /// @param expected The balance a real wrapper would have produced. /// @param actual The balance the candidate produced. error DepositMismatch(uint256 expected, uint256 actual); /// @notice Thrown when unwrapping did not burn the wrapped token. /// @param expected The balance a real wrapper would have left behind. /// @param actual The balance the candidate left behind. error WithdrawMismatch(uint256 expected, uint256 actual); /// @notice Thrown when unwrapping did not return the ether. /// @param expected The ether balance the round trip should have restored. /// @param actual The ether balance it actually left. error EtherNotReturned(uint256 expected, uint256 actual); /// @notice Thrown when ether is sent outside a round trip. error NotProbing(); /// @notice Emitted once, when a chain's wETH is settled. /// @param weth The accepted wETH. /// @param registrar Whoever supplied and paid for it. /// @param codeHash The allowlisted hash its code matched. event Registered(IWETH indexed weth, address indexed registrar, bytes32 codeHash); /// @notice Accept `candidate` as this chain's wETH, if its code is one of /// the reviewed implementations and it wraps ether correctly. /// @dev Send exactly `PROBE` wei. It is not refunded: it stays here, which /// is the cheap way to let a contract register. Paying it back would /// mean calling the registrar with value, and a registrar with no /// payable fallback — a script, a multisig, a deployer contract — /// would then be unable to register at all. One wei, once per chain, /// buys that away. /// /// Reverts rather than degrading when the candidate is unrecognised — /// unreviewed bytecode cannot be shown safe by any test, static or /// behavioural, so a chain running its own wETH is a chain `Love` /// does not deploy on until that implementation is reviewed and /// added. /// @param candidate The wETH to register. /// @return The registered wETH, for the convenience of scripts. function register(IWETH candidate) external payable returns (IWETH) { if (address(weth) != address(0)) revert AlreadyRegistered(weth); if (msg.value != PROBE) revert ProbeValueRequired(msg.value); bytes32 codeHash = address(candidate).codehash; if (!isKnownImplementation(codeHash)) revert UnknownImplementation(candidate, codeHash); _probe(candidate); weth = candidate; emit Registered(candidate, msg.sender, codeHash); return candidate; } /// @notice Whether `codeHash` is one of the reviewed wETH implementations. /// @dev Compile-time, so the list is ownerless and append-only by /// construction: extending it means publishing a new registry, which /// leaves every existing deployment exactly as it was. No key can /// revoke an entry and strand a live `Love`. /// /// These are exact `EXTCODEHASH` values, metadata included. Hashing /// the code with solc's trailing metadata stripped would fold each /// family into a single entry, but it would also accept a known /// implementation followed by arbitrary appended bytes, and the /// safety of that rests on control flow never reaching them — a /// property that holds for every family here and would have to keep /// holding for every family added later. Ten constants is the cheaper /// side of that trade. /// /// Grouped by implementation. Every value is reproducible from chain /// state with `script/weth-codehashes.sh`. /// @param codeHash The `EXTCODEHASH` to check. /// @return True if a candidate with this code may be registered. function isKnownImplementation(bytes32 codeHash) public pure returns (bool) { // OP Stack legacy WETH9, 2041 bytes, solc 0.5.17. One implementation, // four hashes: these chains run byte-identical code and differ only // inside solc's metadata blob, which never executes. if (codeHash == 0x779bbf2a738ef09d961c945116197e2ac764c1b39304b2b4418cd4e42668b173) return true; // optimism if (codeHash == 0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c) return true; // base, mode, zora if (codeHash == 0x557c8e14d33f7cd67cad0141e1a49ebf3488a447fc3df7aa66b127778a0383d1) return true; // world-chain if (codeHash == 0xf35fe602ba2a3b96f2e27ff7c8b8010800a8d0d616a5fb1f902e087b590355f3) return true; // lisk // Canonical WETH9, 3124 bytes, solc 0.4.19. if (codeHash == 0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23) return true; // ethereum if (codeHash == 0xa670ec6c272ddec6d328d6f3d5cad65a841a6ab45e8e5cf825150eb458be4f1f) return true; // linea if (codeHash == 0x032e9cab14331328530468e54f1b91777b4d5c9dbbb400884badb32bc4113585) return true; // polygon-zkevm // OP Stack WETH, 2865 bytes, solc 0.8.15. if (codeHash == 0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012) return true; // unichain, soneium, ink // Taiko, 3204 bytes. if (codeHash == 0x9f3d95086909fce850d997158aba31abe26c3aad6a413107ca0bf9d53a7c42e9) return true; // taiko // Scroll, 5871 bytes. if (codeHash == 0xe8c4073351c26b9831c1e5af153b9be4713a4af9edfdf32b58077b735e120f14) return true; // scroll return false; } /// @notice Take `PROBE` wei through the candidate and back. /// @dev Costs the registrar nothing but gas: the wei returns. Balances are /// read before and after rather than assumed to start at zero, so a /// candidate that was sent wETH beforehand cannot skew the check. /// @param candidate The wETH being probed. function _probe(IWETH candidate) private { uint256 etherBefore = address(this).balance; uint256 wrappedBefore = candidate.balanceOf(address(this)); _probing = true; candidate.deposit{value: PROBE}(); uint256 wrapped = candidate.balanceOf(address(this)); if (wrapped != wrappedBefore + PROBE) revert DepositMismatch(wrappedBefore + PROBE, wrapped); candidate.withdraw(PROBE); uint256 unwrapped = candidate.balanceOf(address(this)); if (unwrapped != wrappedBefore) revert WithdrawMismatch(wrappedBefore, unwrapped); _probing = false; if (address(this).balance != etherBefore) revert EtherNotReturned(etherBefore, address(this).balance); } /// @notice Takes the ether a candidate returns mid-probe, and nothing else. /// @dev The registry is not a wallet. Outside a round trip this reverts, /// so ether cannot be stranded in a contract with no way to move it. receive() external payable { if (!_probing) revert NotProbing(); } }