julien/lovepublic Fork 0
2f484804347452fadfddc5422faa26b86a55b077
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.t.sol · 281 lines · 11.5 KBSolidity Blame HistoryRaw
test the registry gates a34c2e8 julienbrg 8h ago1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.30;
3
4import {IWETH} from "../src/IWETH.sol";
5import {WETHRegistry} from "../src/WETHRegistry.sol";
6import {Fixtures} from "./Fixtures.sol";
7import {MockWETH} from "./mocks/MockWETH.sol";
8import {Test} from "forge-std/Test.sol";
9
10/// @notice A registrar with no payable fallback, which is what a deployer
11/// contract or a multisig looks like from the registry's side.
12contract PlainRegistrar {
13 function register(WETHRegistry registry, IWETH weth) external payable {
14 registry.register{value: msg.value}(weth);
15 }
16}
17
18/// @title WETHRegistryTest
19/// @notice Covers the two gates the registry exists to run — is this code a
20/// reviewed wETH, and does it actually wrap — plus the write-once rule
21/// that stops the token under LOVE's peg from ever being swapped.
22contract WETHRegistryTest is Test, Fixtures {
23 WETHRegistry registry;
24
25 /// @dev Read once in `setUp`. Calling `registry.PROBE()` inline after
26 /// `vm.expectRevert` would bind the expectation to that call instead
27 /// of to `register`.
28 uint256 probe;
29
30 address constant WETH_AT = 0x4200000000000000000000000000000000000006;
31 address constant OTHER_WETH_AT = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
32
33 event Registered(IWETH indexed weth, address indexed registrar, bytes32 codeHash);
34
35 function setUp() public {
36 registry = deployRegistry();
37 probe = registry.PROBE();
38 }
39
40 /*//////////////////////////////////////////////////////////////
41 THE ALLOWLIST
42 //////////////////////////////////////////////////////////////*/
43
44 /// @dev The ten entries, reproducible from chain state with
45 /// `script/weth-codehashes.sh`. Listing them here means a hash edited
46 /// in the contract shows up as a failing test rather than as a silent
47 /// change to what LOVE will accept.
48 function test_KnownImplementations() public view {
49 bytes32[10] memory allowed = [
50 // OP Stack legacy WETH9
51 bytes32(0x779bbf2a738ef09d961c945116197e2ac764c1b39304b2b4418cd4e42668b173),
52 0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c,
53 0x557c8e14d33f7cd67cad0141e1a49ebf3488a447fc3df7aa66b127778a0383d1,
54 0xf35fe602ba2a3b96f2e27ff7c8b8010800a8d0d616a5fb1f902e087b590355f3,
55 // Canonical WETH9
56 0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23,
57 0xa670ec6c272ddec6d328d6f3d5cad65a841a6ab45e8e5cf825150eb458be4f1f,
58 0x032e9cab14331328530468e54f1b91777b4d5c9dbbb400884badb32bc4113585,
59 // OP Stack WETH, solc 0.8.15
60 0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012,
61 // Taiko
62 0x9f3d95086909fce850d997158aba31abe26c3aad6a413107ca0bf9d53a7c42e9,
63 // Scroll
64 0xe8c4073351c26b9831c1e5af153b9be4713a4af9edfdf32b58077b735e120f14
65 ];
66
67 for (uint256 i = 0; i < allowed.length; i++) {
68 assertTrue(registry.isKnownImplementation(allowed[i]), "allowlisted hash rejected");
69 }
70 }
71
72 function test_EmptyCodeHashIsNotKnown() public view {
73 assertFalse(registry.isKnownImplementation(bytes32(0)));
74 assertFalse(registry.isKnownImplementation(keccak256("")));
75 }
76
77 function testFuzz_UnknownHashesAreRejected(bytes32 codeHash) public view {
78 vm.assume(!registry.isKnownImplementation(codeHash));
79 assertFalse(registry.isKnownImplementation(codeHash));
80 }
81
82 /*//////////////////////////////////////////////////////////////
83 REGISTERING
84 //////////////////////////////////////////////////////////////*/
85
86 function test_RegistersOpStackWeth() public {
87 IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
88 register(registry, weth);
89
90 assertEq(address(registry.weth()), WETH_AT);
91 }
92
93 function test_RegistersCanonicalWeth() public {
94 IWETH weth = etchWeth(OTHER_WETH_AT, WETH9_CANONICAL);
95 register(registry, weth);
96
97 assertEq(address(registry.weth()), OTHER_WETH_AT);
98 }
99
100 function test_EmitsRegistered() public {
101 IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
102 deal(address(this), 1 ether);
103
104 vm.expectEmit(true, true, true, true);
105 emit Registered(weth, address(this), WETH_AT.codehash);
106 registry.register{value: probe}(weth);
107 }
108
109 function test_NothingRegisteredInitially() public view {
110 assertEq(address(registry.weth()), address(0));
111 }
112
113 /// @dev The reason the probe wei is not refunded: paying it back means
114 /// calling the registrar with value, which a contract without a
115 /// payable fallback cannot accept.
116 function test_RegistrarWithoutPayableFallbackCanRegister() public {
117 IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
118 PlainRegistrar registrar = new PlainRegistrar();
119 deal(address(this), 1 ether);
120
121 registrar.register{value: probe}(registry, weth);
122
123 assertEq(address(registry.weth()), WETH_AT);
124 }
125
126 function test_ProbeWeiStaysInTheRegistry() public {
127 IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
128 register(registry, weth);
129
130 assertEq(address(registry).balance, probe);
131 }
132
133 /*//////////////////////////////////////////////////////////////
134 THE BYTECODE GATE
135 //////////////////////////////////////////////////////////////*/
136
137 /// @dev `MockWETH` wraps ether correctly and would sail through the
138 /// behavioural probe. It is rejected on its code alone, which is the
139 /// gate doing exactly what it is for: behaving well today says
140 /// nothing about what else the code can do.
141 function test_RevertWhen_CodeIsNotAReviewedImplementation() public {
142 MockWETH mock = new MockWETH();
143 deal(address(this), 1 ether);
144
145 vm.expectRevert(
146 abi.encodeWithSelector(
147 WETHRegistry.UnknownImplementation.selector, IWETH(address(mock)), address(mock).codehash
148 )
149 );
150 registry.register{value: probe}(IWETH(address(mock)));
151 }
152
153 function test_RevertWhen_CandidateHasNoCode() public {
154 address empty = makeAddr("empty");
155 deal(address(this), 1 ether);
156
157 vm.expectRevert(abi.encodeWithSelector(WETHRegistry.UnknownImplementation.selector, IWETH(empty), bytes32(0)));
158 registry.register{value: probe}(IWETH(empty));
159 }
160
161 /*//////////////////////////////////////////////////////////////
162 THE BEHAVIOURAL PROBE
163 //////////////////////////////////////////////////////////////*/
164
165 /// @dev Mocking `balanceOf` leaves the codehash untouched, so the
166 /// candidate still passes the allowlist and the probe is what has to
167 /// catch it. A wrapper that mints nothing on deposit fails here.
168 function test_RevertWhen_DepositMintsNothing() public {
169 IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
170 deal(address(this), 1 ether);
171
172 vm.mockCall(WETH_AT, abi.encodeCall(weth.balanceOf, (address(registry))), abi.encode(uint256(0)));
173
174 vm.expectRevert(abi.encodeWithSelector(WETHRegistry.DepositMismatch.selector, 1, 0));
175 registry.register{value: probe}(weth);
176 }
177
178 /// @dev Mints on deposit, but burns nothing on withdraw: the balance is
179 /// read three times, and the third answer is the one that lies.
180 function test_RevertWhen_WithdrawBurnsNothing() public {
181 IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
182 deal(address(this), 1 ether);
183
184 bytes[] memory balances = new bytes[](3);
185 balances[0] = abi.encode(uint256(0)); // before
186 balances[1] = abi.encode(uint256(1)); // after deposit, correct
187 balances[2] = abi.encode(uint256(1)); // after withdraw, should be 0
188 vm.mockCalls(WETH_AT, abi.encodeCall(weth.balanceOf, (address(registry))), balances);
189
190 vm.expectRevert(abi.encodeWithSelector(WETHRegistry.WithdrawMismatch.selector, 0, 1));
191 registry.register{value: probe}(weth);
192 }
193
194 /// @dev Books the withdrawal correctly but keeps the ether. The balance
195 /// check after the round trip is the only thing that notices.
196 function test_RevertWhen_WithdrawKeepsTheEther() public {
197 IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
198 deal(address(this), 1 ether);
199
200 vm.mockCall(WETH_AT, abi.encodeCall(weth.withdraw, (1)), "");
201
202 bytes[] memory balances = new bytes[](3);
203 balances[0] = abi.encode(uint256(0));
204 balances[1] = abi.encode(uint256(1));
205 balances[2] = abi.encode(uint256(0));
206 vm.mockCalls(WETH_AT, abi.encodeCall(weth.balanceOf, (address(registry))), balances);
207
208 vm.expectRevert(abi.encodeWithSelector(WETHRegistry.EtherNotReturned.selector, 1, 0));
209 registry.register{value: probe}(weth);
210 }
211
212 /*//////////////////////////////////////////////////////////////
213 THE PROBE FEE
214 //////////////////////////////////////////////////////////////*/
215
216 function test_RevertWhen_NoProbeValue() public {
217 IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
218
219 vm.expectRevert(abi.encodeWithSelector(WETHRegistry.ProbeValueRequired.selector, 0));
220 registry.register(weth);
221 }
222
223 function testFuzz_RevertWhen_WrongProbeValue(uint96 value) public {
224 vm.assume(value != probe);
225 IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
226 deal(address(this), uint256(value) + 1 ether);
227
228 vm.expectRevert(abi.encodeWithSelector(WETHRegistry.ProbeValueRequired.selector, value));
229 registry.register{value: value}(weth);
230 }
231
232 /*//////////////////////////////////////////////////////////////
233 WRITE-ONCE
234 //////////////////////////////////////////////////////////////*/
235
236 function test_RevertWhen_AlreadyRegistered() public {
237 IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
238 register(registry, weth);
239
240 deal(address(this), 1 ether);
241 vm.expectRevert(abi.encodeWithSelector(WETHRegistry.AlreadyRegistered.selector, weth));
242 registry.register{value: probe}(weth);
243 }
244
245 /// @dev Including with a second, equally valid wETH — the first answer is
246 /// final, so nothing can move the token under a live peg.
247 function test_RevertWhen_ReplacingWithAnotherValidWeth() public {
248 IWETH first = etchWeth(WETH_AT, WETH9_OP_LEGACY);
249 register(registry, first);
250
251 IWETH second = etchWeth(OTHER_WETH_AT, WETH9_CANONICAL);
252 deal(address(this), 1 ether);
253
254 vm.expectRevert(abi.encodeWithSelector(WETHRegistry.AlreadyRegistered.selector, first));
255 registry.register{value: probe}(second);
256
257 assertEq(address(registry.weth()), WETH_AT);
258 }
259
260 /*//////////////////////////////////////////////////////////////
261 NOT A WALLET
262 //////////////////////////////////////////////////////////////*/
263
264 /// @dev The registry has no owner and no way to move ether out, so it must
265 /// not accept any outside a probe.
266 function test_RevertWhen_EtherSentOutsideAProbe() public {
267 deal(address(this), 1 ether);
268
269 vm.expectRevert(WETHRegistry.NotProbing.selector);
270 payable(address(registry)).transfer(1 ether);
271 }
272
273 function test_RevertWhen_EtherSentAfterRegistration() public {
274 IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
275 register(registry, weth);
276 deal(address(this), 1 ether);
277
278 vm.expectRevert(WETHRegistry.NotProbing.selector);
279 payable(address(registry)).transfer(1 ether);
280 }
281}