| Merge 5-weth-bytecode-verification into main 2f6da5e rickub 6h ago | 1 | // SPDX-License-Identifier: MIT |
| 2 | pragma solidity ^0.8.30; |
| 3 | |
| 4 | import {IWETH} from "./IWETH.sol"; |
| 5 | |
| 6 | /// @title WETHRegistry |
| 7 | /// @author Julien Béranger |
| 8 | /// @notice Names the wETH that `Love` pegs to on this chain, and will only |
| 9 | /// accept an address whose code is one of a fixed set of reviewed wETH |
| 10 | /// implementations. |
| 11 | /// @dev The point is to take the wETH address out of `Love`'s creation code. |
| 12 | /// Anyone can call `register`, on any chain, without a factory, an owner |
| 13 | /// or a per-chain deployer — the contract decides for itself whether to |
| 14 | /// accept the candidate. Because it takes no constructor arguments, its |
| 15 | /// own creation code is identical everywhere, so CREATE2 puts it at one |
| 16 | /// address on every chain and `Love` can hardcode that address. |
| 17 | /// |
| 18 | /// Two gates run at registration: |
| 19 | /// |
| 20 | /// 1. `EXTCODEHASH` against the allowlist below. This is the real |
| 21 | /// defence: matching means the candidate is provably one of a handful |
| 22 | /// of implementations whose source has been read, not merely something |
| 23 | /// that behaves well today. It is what catches a hidden mint, an |
| 24 | /// upgrade hook or a backdoor, none of which a behavioural test can |
| 25 | /// see. |
| 26 | /// 2. A one-wei deposit/withdraw round trip, which catches the plain |
| 27 | /// mistake of an address that is not wETH at all, and proves the |
| 28 | /// wrapper actually works on this chain. |
| 29 | /// |
| 30 | /// The registration is write-once. There is no setter, no owner and no |
| 31 | /// way to revoke an entry, so a `Love` deployed against this registry can |
| 32 | /// never have the token under its peg swapped. |
| 33 | /// |
| 34 | /// Known limitation, and it is not a small one: the allowlist proves a |
| 35 | /// candidate *is* a reviewed wETH implementation, not that it is *the* |
| 36 | /// wETH the chain's ecosystem uses. Anyone can deploy their own copy of |
| 37 | /// WETH9 — identical code, identical codehash, no liquidity — and |
| 38 | /// register it first. The result is still fully backed and redeemable, |
| 39 | /// since it is real WETH9, but it is not the token anyone else holds, and |
| 40 | /// write-once means the mistake cannot be corrected on that chain. So |
| 41 | /// always check `weth()` against the chain's canonical wETH before |
| 42 | /// treating a `Love` instance as the real one; a squatted registry is |
| 43 | /// visible to anyone who looks, and the answer to it is a fresh salt. |
| 44 | contract WETHRegistry { |
| 45 | /// @notice The wETH registered on this chain, or the zero address if none |
| 46 | /// has been registered yet. |
| 47 | /// @dev Written exactly once, by whoever calls `register` first with a |
| 48 | /// candidate that passes both gates. |
| 49 | IWETH public weth; |
| 50 | |
| 51 | /// @notice The ether moved through the candidate to prove it wraps. |
| 52 | /// @dev Stays in the registry afterwards; see `register`. |
| 53 | uint256 public constant PROBE = 1 wei; |
| 54 | |
| 55 | /// @dev Open only for the duration of the round trip, so the registry |
| 56 | /// cannot be used as a place to park ether. Transient, so it costs |
| 57 | /// almost nothing and cannot survive the call that set it. |
| 58 | bool private transient _probing; |
| 59 | |
| 60 | /// @notice Thrown when a wETH has already been registered on this chain. |
| 61 | /// @param registered The wETH registered by the earlier call. |
| 62 | error AlreadyRegistered(IWETH registered); |
| 63 | |
| 64 | /// @notice Thrown when the candidate's code is not a reviewed wETH. |
| 65 | /// @param candidate The rejected address. |
| 66 | /// @param codeHash Its `EXTCODEHASH`, zero if there is no code there. |
| 67 | error UnknownImplementation(IWETH candidate, bytes32 codeHash); |
| 68 | |
| 69 | /// @notice Thrown when the call does not carry exactly `PROBE` wei. |
| 70 | /// @param sent The value that came with the call. |
| 71 | error ProbeValueRequired(uint256 sent); |
| 72 | |
| 73 | /// @notice Thrown when wrapping `PROBE` wei did not mint `PROBE` wrapped. |
| 74 | /// @param expected The balance a real wrapper would have produced. |
| 75 | /// @param actual The balance the candidate produced. |
| 76 | error DepositMismatch(uint256 expected, uint256 actual); |
| 77 | |
| 78 | /// @notice Thrown when unwrapping did not burn the wrapped token. |
| 79 | /// @param expected The balance a real wrapper would have left behind. |
| 80 | /// @param actual The balance the candidate left behind. |
| 81 | error WithdrawMismatch(uint256 expected, uint256 actual); |
| 82 | |
| 83 | /// @notice Thrown when unwrapping did not return the ether. |
| 84 | /// @param expected The ether balance the round trip should have restored. |
| 85 | /// @param actual The ether balance it actually left. |
| 86 | error EtherNotReturned(uint256 expected, uint256 actual); |
| 87 | |
| 88 | /// @notice Thrown when ether is sent outside a round trip. |
| 89 | error NotProbing(); |
| 90 | |
| 91 | /// @notice Emitted once, when a chain's wETH is settled. |
| 92 | /// @param weth The accepted wETH. |
| 93 | /// @param registrar Whoever supplied and paid for it. |
| 94 | /// @param codeHash The allowlisted hash its code matched. |
| 95 | event Registered(IWETH indexed weth, address indexed registrar, bytes32 codeHash); |
| 96 | |
| 97 | /// @notice Accept `candidate` as this chain's wETH, if its code is one of |
| 98 | /// the reviewed implementations and it wraps ether correctly. |
| 99 | /// @dev Send exactly `PROBE` wei. It is not refunded: it stays here, which |
| 100 | /// is the cheap way to let a contract register. Paying it back would |
| 101 | /// mean calling the registrar with value, and a registrar with no |
| 102 | /// payable fallback — a script, a multisig, a deployer contract — |
| 103 | /// would then be unable to register at all. One wei, once per chain, |
| 104 | /// buys that away. |
| 105 | /// |
| 106 | /// Reverts rather than degrading when the candidate is unrecognised — |
| 107 | /// unreviewed bytecode cannot be shown safe by any test, static or |
| 108 | /// behavioural, so a chain running its own wETH is a chain `Love` |
| 109 | /// does not deploy on until that implementation is reviewed and |
| 110 | /// added. |
| 111 | /// @param candidate The wETH to register. |
| 112 | /// @return The registered wETH, for the convenience of scripts. |
| 113 | function register(IWETH candidate) external payable returns (IWETH) { |
| 114 | if (address(weth) != address(0)) revert AlreadyRegistered(weth); |
| 115 | if (msg.value != PROBE) revert ProbeValueRequired(msg.value); |
| 116 | |
| 117 | bytes32 codeHash = address(candidate).codehash; |
| 118 | if (!isKnownImplementation(codeHash)) revert UnknownImplementation(candidate, codeHash); |
| 119 | |
| 120 | _probe(candidate); |
| 121 | |
| 122 | weth = candidate; |
| 123 | emit Registered(candidate, msg.sender, codeHash); |
| 124 | |
| 125 | return candidate; |
| 126 | } |
| 127 | |
| 128 | /// @notice Whether `codeHash` is one of the reviewed wETH implementations. |
| 129 | /// @dev Compile-time, so the list is ownerless and append-only by |
| 130 | /// construction: extending it means publishing a new registry, which |
| 131 | /// leaves every existing deployment exactly as it was. No key can |
| 132 | /// revoke an entry and strand a live `Love`. |
| 133 | /// |
| 134 | /// These are exact `EXTCODEHASH` values, metadata included. Hashing |
| 135 | /// the code with solc's trailing metadata stripped would fold each |
| 136 | /// family into a single entry, but it would also accept a known |
| 137 | /// implementation followed by arbitrary appended bytes, and the |
| 138 | /// safety of that rests on control flow never reaching them — a |
| 139 | /// property that holds for every family here and would have to keep |
| 140 | /// holding for every family added later. Ten constants is the cheaper |
| 141 | /// side of that trade. |
| 142 | /// |
| 143 | /// Grouped by implementation. Every value is reproducible from chain |
| 144 | /// state with `script/weth-codehashes.sh`. |
| 145 | /// @param codeHash The `EXTCODEHASH` to check. |
| 146 | /// @return True if a candidate with this code may be registered. |
| 147 | function isKnownImplementation(bytes32 codeHash) public pure returns (bool) { |
| 148 | // OP Stack legacy WETH9, 2041 bytes, solc 0.5.17. One implementation, |
| 149 | // four hashes: these chains run byte-identical code and differ only |
| 150 | // inside solc's metadata blob, which never executes. |
| 151 | if (codeHash == 0x779bbf2a738ef09d961c945116197e2ac764c1b39304b2b4418cd4e42668b173) return true; // optimism |
| 152 | if (codeHash == 0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c) return true; // base, mode, zora |
| 153 | if (codeHash == 0x557c8e14d33f7cd67cad0141e1a49ebf3488a447fc3df7aa66b127778a0383d1) return true; // world-chain |
| 154 | if (codeHash == 0xf35fe602ba2a3b96f2e27ff7c8b8010800a8d0d616a5fb1f902e087b590355f3) return true; // lisk |
| 155 | |
| 156 | // Canonical WETH9, 3124 bytes, solc 0.4.19. |
| 157 | if (codeHash == 0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23) return true; // ethereum |
| 158 | if (codeHash == 0xa670ec6c272ddec6d328d6f3d5cad65a841a6ab45e8e5cf825150eb458be4f1f) return true; // linea |
| 159 | if (codeHash == 0x032e9cab14331328530468e54f1b91777b4d5c9dbbb400884badb32bc4113585) return true; // polygon-zkevm |
| 160 | |
| 161 | // OP Stack WETH, 2865 bytes, solc 0.8.15. |
| 162 | if (codeHash == 0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012) return true; // unichain, soneium, ink |
| 163 | |
| 164 | // Taiko, 3204 bytes. |
| 165 | if (codeHash == 0x9f3d95086909fce850d997158aba31abe26c3aad6a413107ca0bf9d53a7c42e9) return true; // taiko |
| 166 | |
| 167 | // Scroll, 5871 bytes. |
| 168 | if (codeHash == 0xe8c4073351c26b9831c1e5af153b9be4713a4af9edfdf32b58077b735e120f14) return true; // scroll |
| 169 | |
| 170 | return false; |
| 171 | } |
| 172 | |
| 173 | /// @notice Take `PROBE` wei through the candidate and back. |
| 174 | /// @dev Costs the registrar nothing but gas: the wei returns. Balances are |
| 175 | /// read before and after rather than assumed to start at zero, so a |
| 176 | /// candidate that was sent wETH beforehand cannot skew the check. |
| 177 | /// @param candidate The wETH being probed. |
| 178 | function _probe(IWETH candidate) private { |
| 179 | uint256 etherBefore = address(this).balance; |
| 180 | uint256 wrappedBefore = candidate.balanceOf(address(this)); |
| 181 | |
| 182 | _probing = true; |
| 183 | |
| 184 | candidate.deposit{value: PROBE}(); |
| 185 | uint256 wrapped = candidate.balanceOf(address(this)); |
| 186 | if (wrapped != wrappedBefore + PROBE) revert DepositMismatch(wrappedBefore + PROBE, wrapped); |
| 187 | |
| 188 | candidate.withdraw(PROBE); |
| 189 | uint256 unwrapped = candidate.balanceOf(address(this)); |
| 190 | if (unwrapped != wrappedBefore) revert WithdrawMismatch(wrappedBefore, unwrapped); |
| 191 | |
| 192 | _probing = false; |
| 193 | |
| 194 | if (address(this).balance != etherBefore) revert EtherNotReturned(etherBefore, address(this).balance); |
| 195 | } |
| 196 | |
| 197 | /// @notice Takes the ether a candidate returns mid-probe, and nothing else. |
| 198 | /// @dev The registry is not a wallet. Outside a round trip this reverts, |
| 199 | /// so ether cannot be stranded in a contract with no way to move it. |
| 200 | receive() external payable { |
| 201 | if (!_probing) revert NotProbing(); |
| 202 | } |
| 203 | } |