julien/lovepublic Fork 0
ffa47162b24bdcfe17d608b0671ae87f5461ad88
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 · 203 lines · 10.6 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.
read weth from the registry instead of a constructor arg ffa4716 julienbrg 8h ago52 /// @dev Stays in the registry afterwards; see `register`.
add weth registry gated on bytecode 27d0645 julienbrg 8h ago53 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.
read weth from the registry instead of a constructor arg ffa4716 julienbrg 8h ago99 /// @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 ///
add weth registry gated on bytecode 27d0645 julienbrg 8h ago106 /// 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}