test the registry gates
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 | +} | ||