julien/lovepublic Fork 0
27d0645f962137b8ecb357936d00d540434a11a7
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.

WETHRegistry.sol · 204 lines · 10.5 KBSolidity Blame HistoryRaw
add weth registry gated on bytecode 27d0645 julienbrg 8h ago1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.30;
3
4import {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.
44contract 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}