add weth registry gated on bytecode
27d0645 parent: 5a80e08 added
src/IWETH.sol +20 -0 | new file mode 100644 | ||
| @@ -0,0 +1,20 @@ | ||
| 1 | +// SPDX-License-Identifier: MIT | |
| 2 | +pragma solidity ^0.8.30; | |
| 3 | + | |
| 4 | +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | |
| 5 | + | |
| 6 | +/// @title IWETH | |
| 7 | +/// @author Julien Béranger | |
| 8 | +/// @notice The wrapped-ether surface `WETHRegistry` probes: ERC-20, plus the | |
| 9 | +/// two entrypoints that make a wrapper a wrapper. | |
| 10 | +/// @dev Deliberately not the full WETH9 ABI. The registry only needs to move | |
| 11 | +/// one wei in and back out again, and a narrower interface is a narrower | |
| 12 | +/// set of assumptions about what a candidate has to implement. | |
| 13 | +interface IWETH is IERC20 { | |
| 14 | + /// @notice Wrap the ether sent with this call, one for one. | |
| 15 | + function deposit() external payable; | |
| 16 | + | |
| 17 | + /// @notice Unwrap `amount`, burning the wrapper token and returning ether. | |
| 18 | + /// @param amount The wrapped ether to unwrap, in wei. | |
| 19 | + function withdraw(uint256 amount) external; | |
| 20 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,20 @@ | |||
| 1 | +// SPDX-License-Identifier: MIT | ||
| 2 | +pragma solidity ^0.8.30; | ||
| 3 | + | ||
| 4 | +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
| 5 | + | ||
| 6 | +/// @title IWETH | ||
| 7 | +/// @author Julien Béranger | ||
| 8 | +/// @notice The wrapped-ether surface `WETHRegistry` probes: ERC-20, plus the | ||
| 9 | +/// two entrypoints that make a wrapper a wrapper. | ||
| 10 | +/// @dev Deliberately not the full WETH9 ABI. The registry only needs to move | ||
| 11 | +/// one wei in and back out again, and a narrower interface is a narrower | ||
| 12 | +/// set of assumptions about what a candidate has to implement. | ||
| 13 | +interface IWETH is IERC20 { | ||
| 14 | + /// @notice Wrap the ether sent with this call, one for one. | ||
| 15 | + function deposit() external payable; | ||
| 16 | + | ||
| 17 | + /// @notice Unwrap `amount`, burning the wrapper token and returning ether. | ||
| 18 | + /// @param amount The wrapped ether to unwrap, in wei. | ||
| 19 | + function withdraw(uint256 amount) external; | ||
| 20 | +} | ||
added
src/WETHRegistry.sol +204 -0 | new file mode 100644 | ||
| @@ -0,0 +1,204 @@ | ||
| 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 | + uint256 public constant PROBE = 1 wei; | |
| 53 | + | |
| 54 | + /// @dev Open only for the duration of the round trip, so the registry | |
| 55 | + /// cannot be used as a place to park ether. Transient, so it costs | |
| 56 | + /// almost nothing and cannot survive the call that set it. | |
| 57 | + bool private transient _probing; | |
| 58 | + | |
| 59 | + /// @notice Thrown when a wETH has already been registered on this chain. | |
| 60 | + /// @param registered The wETH registered by the earlier call. | |
| 61 | + error AlreadyRegistered(IWETH registered); | |
| 62 | + | |
| 63 | + /// @notice Thrown when the candidate's code is not a reviewed wETH. | |
| 64 | + /// @param candidate The rejected address. | |
| 65 | + /// @param codeHash Its `EXTCODEHASH`, zero if there is no code there. | |
| 66 | + error UnknownImplementation(IWETH candidate, bytes32 codeHash); | |
| 67 | + | |
| 68 | + /// @notice Thrown when the call does not carry exactly `PROBE` wei. | |
| 69 | + /// @param sent The value that came with the call. | |
| 70 | + error ProbeValueRequired(uint256 sent); | |
| 71 | + | |
| 72 | + /// @notice Thrown when wrapping `PROBE` wei did not mint `PROBE` wrapped. | |
| 73 | + /// @param expected The balance a real wrapper would have produced. | |
| 74 | + /// @param actual The balance the candidate produced. | |
| 75 | + error DepositMismatch(uint256 expected, uint256 actual); | |
| 76 | + | |
| 77 | + /// @notice Thrown when unwrapping did not burn the wrapped token. | |
| 78 | + /// @param expected The balance a real wrapper would have left behind. | |
| 79 | + /// @param actual The balance the candidate left behind. | |
| 80 | + error WithdrawMismatch(uint256 expected, uint256 actual); | |
| 81 | + | |
| 82 | + /// @notice Thrown when unwrapping did not return the ether. | |
| 83 | + /// @param expected The ether balance the round trip should have restored. | |
| 84 | + /// @param actual The ether balance it actually left. | |
| 85 | + error EtherNotReturned(uint256 expected, uint256 actual); | |
| 86 | + | |
| 87 | + /// @notice Thrown when ether is sent outside a round trip. | |
| 88 | + error NotProbing(); | |
| 89 | + | |
| 90 | + /// @notice Thrown when the probe wei could not be sent back. | |
| 91 | + error RefundFailed(); | |
| 92 | + | |
| 93 | + /// @notice Emitted once, when a chain's wETH is settled. | |
| 94 | + /// @param weth The accepted wETH. | |
| 95 | + /// @param registrar Whoever supplied and paid for it. | |
| 96 | + /// @param codeHash The allowlisted hash its code matched. | |
| 97 | + event Registered(IWETH indexed weth, address indexed registrar, bytes32 codeHash); | |
| 98 | + | |
| 99 | + /// @notice Accept `candidate` as this chain's wETH, if its code is one of | |
| 100 | + /// the reviewed implementations and it wraps ether correctly. | |
| 101 | + /// @dev Send exactly `PROBE` wei; it makes the round trip and comes back. | |
| 102 | + /// Reverts rather than degrading when the candidate is unrecognised — | |
| 103 | + /// unreviewed bytecode cannot be shown safe by any test, static or | |
| 104 | + /// behavioural, so a chain running its own wETH is a chain `Love` | |
| 105 | + /// does not deploy on until that implementation is reviewed and | |
| 106 | + /// added. | |
| 107 | + /// @param candidate The wETH to register. | |
| 108 | + /// @return The registered wETH, for the convenience of scripts. | |
| 109 | + function register(IWETH candidate) external payable returns (IWETH) { | |
| 110 | + if (address(weth) != address(0)) revert AlreadyRegistered(weth); | |
| 111 | + if (msg.value != PROBE) revert ProbeValueRequired(msg.value); | |
| 112 | + | |
| 113 | + bytes32 codeHash = address(candidate).codehash; | |
| 114 | + if (!isKnownImplementation(codeHash)) revert UnknownImplementation(candidate, codeHash); | |
| 115 | + | |
| 116 | + _probe(candidate); | |
| 117 | + | |
| 118 | + weth = candidate; | |
| 119 | + emit Registered(candidate, msg.sender, codeHash); | |
| 120 | + | |
| 121 | + // After the state is settled, so a registrar that re-enters here finds | |
| 122 | + // the registry already closed. | |
| 123 | + (bool ok,) = msg.sender.call{value: PROBE}(""); | |
| 124 | + if (!ok) revert RefundFailed(); | |
| 125 | + | |
| 126 | + return candidate; | |
| 127 | + } | |
| 128 | + | |
| 129 | + /// @notice Whether `codeHash` is one of the reviewed wETH implementations. | |
| 130 | + /// @dev Compile-time, so the list is ownerless and append-only by | |
| 131 | + /// construction: extending it means publishing a new registry, which | |
| 132 | + /// leaves every existing deployment exactly as it was. No key can | |
| 133 | + /// revoke an entry and strand a live `Love`. | |
| 134 | + /// | |
| 135 | + /// These are exact `EXTCODEHASH` values, metadata included. Hashing | |
| 136 | + /// the code with solc's trailing metadata stripped would fold each | |
| 137 | + /// family into a single entry, but it would also accept a known | |
| 138 | + /// implementation followed by arbitrary appended bytes, and the | |
| 139 | + /// safety of that rests on control flow never reaching them — a | |
| 140 | + /// property that holds for every family here and would have to keep | |
| 141 | + /// holding for every family added later. Ten constants is the cheaper | |
| 142 | + /// side of that trade. | |
| 143 | + /// | |
| 144 | + /// Grouped by implementation. Every value is reproducible from chain | |
| 145 | + /// state with `script/weth-codehashes.sh`. | |
| 146 | + /// @param codeHash The `EXTCODEHASH` to check. | |
| 147 | + /// @return True if a candidate with this code may be registered. | |
| 148 | + function isKnownImplementation(bytes32 codeHash) public pure returns (bool) { | |
| 149 | + // OP Stack legacy WETH9, 2041 bytes, solc 0.5.17. One implementation, | |
| 150 | + // four hashes: these chains run byte-identical code and differ only | |
| 151 | + // inside solc's metadata blob, which never executes. | |
| 152 | + if (codeHash == 0x779bbf2a738ef09d961c945116197e2ac764c1b39304b2b4418cd4e42668b173) return true; // optimism | |
| 153 | + if (codeHash == 0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c) return true; // base, mode, zora | |
| 154 | + if (codeHash == 0x557c8e14d33f7cd67cad0141e1a49ebf3488a447fc3df7aa66b127778a0383d1) return true; // world-chain | |
| 155 | + if (codeHash == 0xf35fe602ba2a3b96f2e27ff7c8b8010800a8d0d616a5fb1f902e087b590355f3) return true; // lisk | |
| 156 | + | |
| 157 | + // Canonical WETH9, 3124 bytes, solc 0.4.19. | |
| 158 | + if (codeHash == 0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23) return true; // ethereum | |
| 159 | + if (codeHash == 0xa670ec6c272ddec6d328d6f3d5cad65a841a6ab45e8e5cf825150eb458be4f1f) return true; // linea | |
| 160 | + if (codeHash == 0x032e9cab14331328530468e54f1b91777b4d5c9dbbb400884badb32bc4113585) return true; // polygon-zkevm | |
| 161 | + | |
| 162 | + // OP Stack WETH, 2865 bytes, solc 0.8.15. | |
| 163 | + if (codeHash == 0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012) return true; // unichain, soneium, ink | |
| 164 | + | |
| 165 | + // Taiko, 3204 bytes. | |
| 166 | + if (codeHash == 0x9f3d95086909fce850d997158aba31abe26c3aad6a413107ca0bf9d53a7c42e9) return true; // taiko | |
| 167 | + | |
| 168 | + // Scroll, 5871 bytes. | |
| 169 | + if (codeHash == 0xe8c4073351c26b9831c1e5af153b9be4713a4af9edfdf32b58077b735e120f14) return true; // scroll | |
| 170 | + | |
| 171 | + return false; | |
| 172 | + } | |
| 173 | + | |
| 174 | + /// @notice Take `PROBE` wei through the candidate and back. | |
| 175 | + /// @dev Costs the registrar nothing but gas: the wei returns. Balances are | |
| 176 | + /// read before and after rather than assumed to start at zero, so a | |
| 177 | + /// candidate that was sent wETH beforehand cannot skew the check. | |
| 178 | + /// @param candidate The wETH being probed. | |
| 179 | + function _probe(IWETH candidate) private { | |
| 180 | + uint256 etherBefore = address(this).balance; | |
| 181 | + uint256 wrappedBefore = candidate.balanceOf(address(this)); | |
| 182 | + | |
| 183 | + _probing = true; | |
| 184 | + | |
| 185 | + candidate.deposit{value: PROBE}(); | |
| 186 | + uint256 wrapped = candidate.balanceOf(address(this)); | |
| 187 | + if (wrapped != wrappedBefore + PROBE) revert DepositMismatch(wrappedBefore + PROBE, wrapped); | |
| 188 | + | |
| 189 | + candidate.withdraw(PROBE); | |
| 190 | + uint256 unwrapped = candidate.balanceOf(address(this)); | |
| 191 | + if (unwrapped != wrappedBefore) revert WithdrawMismatch(wrappedBefore, unwrapped); | |
| 192 | + | |
| 193 | + _probing = false; | |
| 194 | + | |
| 195 | + if (address(this).balance != etherBefore) revert EtherNotReturned(etherBefore, address(this).balance); | |
| 196 | + } | |
| 197 | + | |
| 198 | + /// @notice Takes the ether a candidate returns mid-probe, and nothing else. | |
| 199 | + /// @dev The registry is not a wallet. Outside a round trip this reverts, | |
| 200 | + /// so ether cannot be stranded in a contract with no way to move it. | |
| 201 | + receive() external payable { | |
| 202 | + if (!_probing) revert NotProbing(); | |
| 203 | + } | |
| 204 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,204 @@ | |||
| 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 | + uint256 public constant PROBE = 1 wei; | ||
| 53 | + | ||
| 54 | + /// @dev Open only for the duration of the round trip, so the registry | ||
| 55 | + /// cannot be used as a place to park ether. Transient, so it costs | ||
| 56 | + /// almost nothing and cannot survive the call that set it. | ||
| 57 | + bool private transient _probing; | ||
| 58 | + | ||
| 59 | + /// @notice Thrown when a wETH has already been registered on this chain. | ||
| 60 | + /// @param registered The wETH registered by the earlier call. | ||
| 61 | + error AlreadyRegistered(IWETH registered); | ||
| 62 | + | ||
| 63 | + /// @notice Thrown when the candidate's code is not a reviewed wETH. | ||
| 64 | + /// @param candidate The rejected address. | ||
| 65 | + /// @param codeHash Its `EXTCODEHASH`, zero if there is no code there. | ||
| 66 | + error UnknownImplementation(IWETH candidate, bytes32 codeHash); | ||
| 67 | + | ||
| 68 | + /// @notice Thrown when the call does not carry exactly `PROBE` wei. | ||
| 69 | + /// @param sent The value that came with the call. | ||
| 70 | + error ProbeValueRequired(uint256 sent); | ||
| 71 | + | ||
| 72 | + /// @notice Thrown when wrapping `PROBE` wei did not mint `PROBE` wrapped. | ||
| 73 | + /// @param expected The balance a real wrapper would have produced. | ||
| 74 | + /// @param actual The balance the candidate produced. | ||
| 75 | + error DepositMismatch(uint256 expected, uint256 actual); | ||
| 76 | + | ||
| 77 | + /// @notice Thrown when unwrapping did not burn the wrapped token. | ||
| 78 | + /// @param expected The balance a real wrapper would have left behind. | ||
| 79 | + /// @param actual The balance the candidate left behind. | ||
| 80 | + error WithdrawMismatch(uint256 expected, uint256 actual); | ||
| 81 | + | ||
| 82 | + /// @notice Thrown when unwrapping did not return the ether. | ||
| 83 | + /// @param expected The ether balance the round trip should have restored. | ||
| 84 | + /// @param actual The ether balance it actually left. | ||
| 85 | + error EtherNotReturned(uint256 expected, uint256 actual); | ||
| 86 | + | ||
| 87 | + /// @notice Thrown when ether is sent outside a round trip. | ||
| 88 | + error NotProbing(); | ||
| 89 | + | ||
| 90 | + /// @notice Thrown when the probe wei could not be sent back. | ||
| 91 | + error RefundFailed(); | ||
| 92 | + | ||
| 93 | + /// @notice Emitted once, when a chain's wETH is settled. | ||
| 94 | + /// @param weth The accepted wETH. | ||
| 95 | + /// @param registrar Whoever supplied and paid for it. | ||
| 96 | + /// @param codeHash The allowlisted hash its code matched. | ||
| 97 | + event Registered(IWETH indexed weth, address indexed registrar, bytes32 codeHash); | ||
| 98 | + | ||
| 99 | + /// @notice Accept `candidate` as this chain's wETH, if its code is one of | ||
| 100 | + /// the reviewed implementations and it wraps ether correctly. | ||
| 101 | + /// @dev Send exactly `PROBE` wei; it makes the round trip and comes back. | ||
| 102 | + /// Reverts rather than degrading when the candidate is unrecognised — | ||
| 103 | + /// unreviewed bytecode cannot be shown safe by any test, static or | ||
| 104 | + /// behavioural, so a chain running its own wETH is a chain `Love` | ||
| 105 | + /// does not deploy on until that implementation is reviewed and | ||
| 106 | + /// added. | ||
| 107 | + /// @param candidate The wETH to register. | ||
| 108 | + /// @return The registered wETH, for the convenience of scripts. | ||
| 109 | + function register(IWETH candidate) external payable returns (IWETH) { | ||
| 110 | + if (address(weth) != address(0)) revert AlreadyRegistered(weth); | ||
| 111 | + if (msg.value != PROBE) revert ProbeValueRequired(msg.value); | ||
| 112 | + | ||
| 113 | + bytes32 codeHash = address(candidate).codehash; | ||
| 114 | + if (!isKnownImplementation(codeHash)) revert UnknownImplementation(candidate, codeHash); | ||
| 115 | + | ||
| 116 | + _probe(candidate); | ||
| 117 | + | ||
| 118 | + weth = candidate; | ||
| 119 | + emit Registered(candidate, msg.sender, codeHash); | ||
| 120 | + | ||
| 121 | + // After the state is settled, so a registrar that re-enters here finds | ||
| 122 | + // the registry already closed. | ||
| 123 | + (bool ok,) = msg.sender.call{value: PROBE}(""); | ||
| 124 | + if (!ok) revert RefundFailed(); | ||
| 125 | + | ||
| 126 | + return candidate; | ||
| 127 | + } | ||
| 128 | + | ||
| 129 | + /// @notice Whether `codeHash` is one of the reviewed wETH implementations. | ||
| 130 | + /// @dev Compile-time, so the list is ownerless and append-only by | ||
| 131 | + /// construction: extending it means publishing a new registry, which | ||
| 132 | + /// leaves every existing deployment exactly as it was. No key can | ||
| 133 | + /// revoke an entry and strand a live `Love`. | ||
| 134 | + /// | ||
| 135 | + /// These are exact `EXTCODEHASH` values, metadata included. Hashing | ||
| 136 | + /// the code with solc's trailing metadata stripped would fold each | ||
| 137 | + /// family into a single entry, but it would also accept a known | ||
| 138 | + /// implementation followed by arbitrary appended bytes, and the | ||
| 139 | + /// safety of that rests on control flow never reaching them — a | ||
| 140 | + /// property that holds for every family here and would have to keep | ||
| 141 | + /// holding for every family added later. Ten constants is the cheaper | ||
| 142 | + /// side of that trade. | ||
| 143 | + /// | ||
| 144 | + /// Grouped by implementation. Every value is reproducible from chain | ||
| 145 | + /// state with `script/weth-codehashes.sh`. | ||
| 146 | + /// @param codeHash The `EXTCODEHASH` to check. | ||
| 147 | + /// @return True if a candidate with this code may be registered. | ||
| 148 | + function isKnownImplementation(bytes32 codeHash) public pure returns (bool) { | ||
| 149 | + // OP Stack legacy WETH9, 2041 bytes, solc 0.5.17. One implementation, | ||
| 150 | + // four hashes: these chains run byte-identical code and differ only | ||
| 151 | + // inside solc's metadata blob, which never executes. | ||
| 152 | + if (codeHash == 0x779bbf2a738ef09d961c945116197e2ac764c1b39304b2b4418cd4e42668b173) return true; // optimism | ||
| 153 | + if (codeHash == 0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c) return true; // base, mode, zora | ||
| 154 | + if (codeHash == 0x557c8e14d33f7cd67cad0141e1a49ebf3488a447fc3df7aa66b127778a0383d1) return true; // world-chain | ||
| 155 | + if (codeHash == 0xf35fe602ba2a3b96f2e27ff7c8b8010800a8d0d616a5fb1f902e087b590355f3) return true; // lisk | ||
| 156 | + | ||
| 157 | + // Canonical WETH9, 3124 bytes, solc 0.4.19. | ||
| 158 | + if (codeHash == 0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23) return true; // ethereum | ||
| 159 | + if (codeHash == 0xa670ec6c272ddec6d328d6f3d5cad65a841a6ab45e8e5cf825150eb458be4f1f) return true; // linea | ||
| 160 | + if (codeHash == 0x032e9cab14331328530468e54f1b91777b4d5c9dbbb400884badb32bc4113585) return true; // polygon-zkevm | ||
| 161 | + | ||
| 162 | + // OP Stack WETH, 2865 bytes, solc 0.8.15. | ||
| 163 | + if (codeHash == 0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012) return true; // unichain, soneium, ink | ||
| 164 | + | ||
| 165 | + // Taiko, 3204 bytes. | ||
| 166 | + if (codeHash == 0x9f3d95086909fce850d997158aba31abe26c3aad6a413107ca0bf9d53a7c42e9) return true; // taiko | ||
| 167 | + | ||
| 168 | + // Scroll, 5871 bytes. | ||
| 169 | + if (codeHash == 0xe8c4073351c26b9831c1e5af153b9be4713a4af9edfdf32b58077b735e120f14) return true; // scroll | ||
| 170 | + | ||
| 171 | + return false; | ||
| 172 | + } | ||
| 173 | + | ||
| 174 | + /// @notice Take `PROBE` wei through the candidate and back. | ||
| 175 | + /// @dev Costs the registrar nothing but gas: the wei returns. Balances are | ||
| 176 | + /// read before and after rather than assumed to start at zero, so a | ||
| 177 | + /// candidate that was sent wETH beforehand cannot skew the check. | ||
| 178 | + /// @param candidate The wETH being probed. | ||
| 179 | + function _probe(IWETH candidate) private { | ||
| 180 | + uint256 etherBefore = address(this).balance; | ||
| 181 | + uint256 wrappedBefore = candidate.balanceOf(address(this)); | ||
| 182 | + | ||
| 183 | + _probing = true; | ||
| 184 | + | ||
| 185 | + candidate.deposit{value: PROBE}(); | ||
| 186 | + uint256 wrapped = candidate.balanceOf(address(this)); | ||
| 187 | + if (wrapped != wrappedBefore + PROBE) revert DepositMismatch(wrappedBefore + PROBE, wrapped); | ||
| 188 | + | ||
| 189 | + candidate.withdraw(PROBE); | ||
| 190 | + uint256 unwrapped = candidate.balanceOf(address(this)); | ||
| 191 | + if (unwrapped != wrappedBefore) revert WithdrawMismatch(wrappedBefore, unwrapped); | ||
| 192 | + | ||
| 193 | + _probing = false; | ||
| 194 | + | ||
| 195 | + if (address(this).balance != etherBefore) revert EtherNotReturned(etherBefore, address(this).balance); | ||
| 196 | + } | ||
| 197 | + | ||
| 198 | + /// @notice Takes the ether a candidate returns mid-probe, and nothing else. | ||
| 199 | + /// @dev The registry is not a wallet. Outside a round trip this reverts, | ||
| 200 | + /// so ether cannot be stranded in a contract with no way to move it. | ||
| 201 | + receive() external payable { | ||
| 202 | + if (!_probing) revert NotProbing(); | ||
| 203 | + } | ||
| 204 | +} | ||