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

test the registry gates

julienbrg committed 2026-09-17T23:39:37+02:00 Browse files
a34c2e8 parent: ffa4716
added test/WETHRegistry.t.sol +281 -0
new file mode 100644
@@ -0,0 +1,281 @@
1+// SPDX-License-Identifier: MIT
2+pragma solidity ^0.8.30;
3+
4+import {IWETH} from "../src/IWETH.sol";
5+import {WETHRegistry} from "../src/WETHRegistry.sol";
6+import {Fixtures} from "./Fixtures.sol";
7+import {MockWETH} from "./mocks/MockWETH.sol";
8+import {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.
12+contract 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.
22+contract 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+}
new file mode 100644
@@ -0,0 +1,281 @@
1+// SPDX-License-Identifier: MIT
2+pragma solidity ^0.8.30;
3+
4+import {IWETH} from "../src/IWETH.sol";
5+import {WETHRegistry} from "../src/WETHRegistry.sol";
6+import {Fixtures} from "./Fixtures.sol";
7+import {MockWETH} from "./mocks/MockWETH.sol";
8+import {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.
12+contract 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.
22+contract 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+}