read weth from the registry instead of a constructor arg
ffa4716 parent: 27d0645 modified
foundry.toml +4 -1 | @@ -11,7 +11,10 @@ optimizer_runs = 200 | ||
| 11 | 11 | via_ir = false |
| 12 | 12 | bytecode_hash = "none" |
| 13 | 13 | verbosity = 2 |
| 14 | -fs_permissions = [{ access = "read", path = "./out" }] | |
| 14 | +fs_permissions = [ | |
| 15 | + { access = "read", path = "./out" }, | |
| 16 | + { access = "read", path = "./test/fixtures" }, | |
| 17 | +] | |
| 15 | 18 | |
| 16 | 19 | [profile.ci] |
| 17 | 20 | fuzz = { runs = 10_000 } |
| @@ -11,7 +11,10 @@ optimizer_runs = 200 | |||
| 11 | via_ir = false | 11 | via_ir = false |
| 12 | bytecode_hash = "none" | 12 | bytecode_hash = "none" |
| 13 | verbosity = 2 | 13 | verbosity = 2 |
| 14 | -fs_permissions = [{ access = "read", path = "./out" }] | 14 | +fs_permissions = [ |
| 15 | + { access = "read", path = "./out" }, | ||
| 16 | + { access = "read", path = "./test/fixtures" }, | ||
| 17 | +] | ||
| 15 | 18 | ||
| 16 | [profile.ci] | 19 | [profile.ci] |
| 17 | fuzz = { runs = 10_000 } | 20 | fuzz = { runs = 10_000 } |
modified
script/Love.s.sol +135 -59 | @@ -1,112 +1,188 @@ | ||
| 1 | 1 | // SPDX-License-Identifier: MIT |
| 2 | 2 | pragma solidity ^0.8.30; |
| 3 | 3 | |
| 4 | +import {IWETH} from "../src/IWETH.sol"; | |
| 4 | 5 | import {Love} from "../src/Love.sol"; |
| 5 | -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | |
| 6 | +import {WETHRegistry} from "../src/WETHRegistry.sol"; | |
| 6 | 7 | import {Script, console} from "forge-std/Script.sol"; |
| 7 | 8 | |
| 8 | 9 | /// @title LoveScript |
| 9 | 10 | /// @author Julien Béranger |
| 10 | -/// @notice Deploys `Love` with CREATE2 through the canonical deterministic | |
| 11 | -/// deployer. The address depends on the salt and the creation code, and | |
| 12 | -/// the creation code embeds the wETH address — so the token only gets | |
| 13 | -/// the same address on chains where wETH sits at the same address. Keep | |
| 14 | -/// `solc`, the optimizer settings and `bytecode_hash = "none"` as they | |
| 15 | -/// are in `foundry.toml`, or the address changes too. | |
| 11 | +/// @notice Brings LOVE up on a chain, in the order the design requires: | |
| 12 | +/// registry first, wETH registered second, token third. | |
| 13 | +/// @dev Both contracts go through the canonical deterministic deployer and | |
| 14 | +/// neither takes constructor arguments, so their creation code is | |
| 15 | +/// identical on every chain and so are their addresses. That is the whole | |
| 16 | +/// point — LOVE is one address everywhere, not one per wETH deployment. | |
| 17 | +/// Keep `solc`, the optimizer settings and `bytecode_hash = "none"` as | |
| 18 | +/// they are in `foundry.toml`, or both addresses move. | |
| 16 | 19 | contract LoveScript is Script { |
| 17 | - /// @notice Salt used when `SALT` is not set in the environment. | |
| 20 | + /// @notice Salt used for `Love` when `SALT` is not set in the environment. | |
| 18 | 21 | bytes32 public constant DEFAULT_SALT = keccak256("LOVE"); |
| 19 | 22 | |
| 23 | + /// @notice Salt the registry is deployed with. Not configurable: `Love` | |
| 24 | + /// has the resulting address compiled into it. | |
| 25 | + bytes32 public constant REGISTRY_SALT = keccak256("LOVE.WETHRegistry"); | |
| 26 | + | |
| 20 | 27 | /// @notice wETH address used when `WETH` is not set in the environment. |
| 21 | - /// @dev wETH on Base, Optimism and every other OP-Stack chain. | |
| 28 | + /// @dev The OP Stack predeploy, which is where wETH sits on Optimism, | |
| 29 | + /// Base, Mode, Zora, Lisk, World Chain, Unichain, Soneium and Ink. | |
| 30 | + /// Chains outside that set need `WETH` set explicitly. | |
| 22 | 31 | address public constant DEFAULT_WETH = 0x4200000000000000000000000000000000000006; |
| 23 | 32 | |
| 33 | + /// @notice The registry, once `run()` has deployed or found it. | |
| 34 | + WETHRegistry public registry; | |
| 35 | + | |
| 24 | 36 | /// @notice The token, once `run()` has deployed or found it. |
| 25 | 37 | Love public love; |
| 26 | 38 | |
| 27 | - /// @notice Deploy `Love`, or return it untouched if it is already there. | |
| 39 | + /// @notice Deploy the registry, register wETH and deploy the token, | |
| 40 | + /// skipping whichever of those has already happened. | |
| 28 | 41 | /// @dev Reads `SALT` and `WETH` from the environment, falling back to the |
| 29 | - /// defaults, and asserts the deployed address matches the prediction. | |
| 42 | + /// defaults, and asserts both deployments match their predictions. | |
| 30 | 43 | /// @return The deployed token. |
| 31 | 44 | function run() public returns (Love) { |
| 32 | - bytes32 s = salt(); | |
| 33 | 45 | address w = wethAddress(); |
| 34 | - address predicted = predict(s, w); | |
| 46 | + address predictedRegistry = registryAddress(); | |
| 47 | + address predicted = predict(salt()); | |
| 35 | 48 | |
| 36 | 49 | console.log("deployer ", CREATE2_FACTORY); |
| 37 | - console.log("salt ", vm.toString(s)); | |
| 50 | + console.log("registry ", predictedRegistry); | |
| 38 | 51 | console.log("weth ", w); |
| 39 | - console.log("initCodeHash ", vm.toString(initCodeHash(w))); | |
| 52 | + console.log("salt ", vm.toString(salt())); | |
| 53 | + console.log("initCodeHash ", vm.toString(initCodeHash())); | |
| 40 | 54 | console.log("predicted ", predicted); |
| 41 | 55 | |
| 42 | - if (predicted.code.length > 0) { | |
| 43 | - console.log("already deployed, nothing to do"); | |
| 44 | - love = Love(predicted); | |
| 45 | - return love; | |
| 46 | - } | |
| 47 | - | |
| 48 | - vm.startBroadcast(); | |
| 49 | - love = new Love{salt: s}(IERC20(w)); | |
| 50 | - vm.stopBroadcast(); | |
| 51 | - | |
| 52 | - require(address(love) == predicted, "LoveScript: address mismatch"); | |
| 53 | - | |
| 54 | - console.log("Love deployed at", address(love)); | |
| 55 | - console.log("name ", love.name()); | |
| 56 | - console.log("symbol ", love.symbol()); | |
| 57 | - console.log("weth ", address(love.WETH())); | |
| 58 | - console.log("rate ", love.RATE()); | |
| 59 | - console.log("totalSupply ", love.totalSupply()); | |
| 56 | + _ensureRegistry(predictedRegistry); | |
| 57 | + _ensureRegistered(IWETH(w)); | |
| 58 | + _ensureLove(predicted); | |
| 60 | 59 | |
| 61 | 60 | return love; |
| 62 | 61 | } |
| 63 | 62 | |
| 64 | - /// @notice Print the address `run()` would deploy to, without broadcasting. | |
| 65 | - /// @return predicted The address the current salt and wETH derive to. | |
| 63 | + /// @notice Print what `run()` would deploy, without broadcasting. | |
| 64 | + /// @return predicted The address the current salt derives to. | |
| 66 | 65 | function predict() public view returns (address predicted) { |
| 67 | - bytes32 s = salt(); | |
| 68 | - address w = wethAddress(); | |
| 69 | - predicted = predict(s, w); | |
| 66 | + predicted = predict(salt()); | |
| 70 | 67 | |
| 71 | - console.log("salt ", vm.toString(s)); | |
| 72 | - console.log("weth ", w); | |
| 73 | - console.log("initCodeHash ", vm.toString(initCodeHash(w))); | |
| 68 | + console.log("registry ", registryAddress()); | |
| 69 | + console.log("weth ", wethAddress()); | |
| 70 | + console.log("salt ", vm.toString(salt())); | |
| 71 | + console.log("initCodeHash ", vm.toString(initCodeHash())); | |
| 74 | 72 | console.log("predicted ", predicted); |
| 75 | 73 | } |
| 76 | 74 | |
| 77 | - /// @notice Derive the deployment address from a salt and a wETH address. | |
| 75 | + /// @notice Derive the token's deployment address from a salt. | |
| 76 | + /// @dev Takes no wETH argument, unlike the address derivation this | |
| 77 | + /// replaces: wETH is no longer in the creation code, which is why the | |
| 78 | + /// answer is the same on every chain. | |
| 78 | 79 | /// @param s The CREATE2 salt. |
| 79 | - /// @param w The wETH the token would be pegged to. | |
| 80 | 80 | /// @return The address `Love` would land at. |
| 81 | - function predict(bytes32 s, address w) public pure returns (address) { | |
| 82 | - return vm.computeCreate2Address(s, initCodeHash(w), CREATE2_FACTORY); | |
| 81 | + function predict(bytes32 s) public pure returns (address) { | |
| 82 | + return vm.computeCreate2Address(s, initCodeHash(), CREATE2_FACTORY); | |
| 83 | + } | |
| 84 | + | |
| 85 | + /// @notice The address the registry lands at on every chain. | |
| 86 | + /// @return The registry's CREATE2 address. | |
| 87 | + function registryAddress() public pure returns (address) { | |
| 88 | + return vm.computeCreate2Address(REGISTRY_SALT, keccak256(registryInitCode()), CREATE2_FACTORY); | |
| 83 | 89 | } |
| 84 | 90 | |
| 85 | - /// @notice The creation code CREATE2 is handed, constructor argument included. | |
| 86 | - /// @param w The wETH the token would be pegged to. | |
| 87 | - /// @return The creation code, with `w` ABI-encoded onto it. | |
| 88 | - function initCode(address w) public pure returns (bytes memory) { | |
| 89 | - return abi.encodePacked(type(Love).creationCode, abi.encode(w)); | |
| 91 | + /// @notice The token's creation code, which now carries no arguments. | |
| 92 | + /// @return The creation code handed to CREATE2. | |
| 93 | + function initCode() public pure returns (bytes memory) { | |
| 94 | + return type(Love).creationCode; | |
| 90 | 95 | } |
| 91 | 96 | |
| 92 | - /// @notice Hash of the creation code, the second input to the address derivation. | |
| 93 | - /// @param w The wETH the token would be pegged to. | |
| 94 | - /// @return The keccak256 of `initCode(w)`. | |
| 95 | - function initCodeHash(address w) public pure returns (bytes32) { | |
| 96 | - return keccak256(initCode(w)); | |
| 97 | + /// @notice Hash of the token's creation code. | |
| 98 | + /// @return The keccak256 of `initCode()`. | |
| 99 | + function initCodeHash() public pure returns (bytes32) { | |
| 100 | + return keccak256(initCode()); | |
| 97 | 101 | } |
| 98 | 102 | |
| 99 | - /// @notice The salt to deploy with. | |
| 100 | - /// @dev `SALT` overrides the default, e.g. to mine a vanity address. | |
| 103 | + /// @notice The registry's creation code. | |
| 104 | + /// @return The creation code handed to CREATE2. | |
| 105 | + function registryInitCode() public pure returns (bytes memory) { | |
| 106 | + return type(WETHRegistry).creationCode; | |
| 107 | + } | |
| 108 | + | |
| 109 | + /// @notice The salt to deploy the token with. | |
| 110 | + /// @dev `SALT` overrides the default, e.g. to mine a vanity address or to | |
| 111 | + /// route around a chain where the registry has been squatted. | |
| 101 | 112 | /// @return The configured salt, or `DEFAULT_SALT`. |
| 102 | 113 | function salt() public view returns (bytes32) { |
| 103 | 114 | return vm.envOr("SALT", DEFAULT_SALT); |
| 104 | 115 | } |
| 105 | 116 | |
| 106 | - /// @notice The wETH address to peg the deployment to. | |
| 107 | - /// @dev `WETH` overrides the default, which only holds on OP-Stack chains. | |
| 117 | + /// @notice The wETH address to register on this chain. | |
| 118 | + /// @dev `WETH` overrides the default, which only holds on OP Stack chains. | |
| 119 | + /// Ignored once a wETH is already registered. | |
| 108 | 120 | /// @return The configured wETH address, or `DEFAULT_WETH`. |
| 109 | 121 | function wethAddress() public view returns (address) { |
| 110 | 122 | return vm.envOr("WETH", DEFAULT_WETH); |
| 111 | 123 | } |
| 124 | + | |
| 125 | + /// @dev Deploys the registry unless it is already there. | |
| 126 | + /// @param predicted Where it should land. | |
| 127 | + function _ensureRegistry(address predicted) private { | |
| 128 | + if (predicted.code.length > 0) { | |
| 129 | + console.log("registry already deployed"); | |
| 130 | + registry = WETHRegistry(payable(predicted)); | |
| 131 | + return; | |
| 132 | + } | |
| 133 | + | |
| 134 | + vm.startBroadcast(); | |
| 135 | + registry = new WETHRegistry{salt: REGISTRY_SALT}(); | |
| 136 | + vm.stopBroadcast(); | |
| 137 | + | |
| 138 | + require(address(registry) == predicted, "LoveScript: registry address mismatch"); | |
| 139 | + console.log("registry deployed at", address(registry)); | |
| 140 | + } | |
| 141 | + | |
| 142 | + /// @dev Registers `candidate` unless a wETH is already registered. Reverts | |
| 143 | + /// inside the registry if the candidate's code is not a reviewed wETH | |
| 144 | + /// — which is the intended outcome on a chain whose wETH has not been | |
| 145 | + /// reviewed, not a script failure to work around. | |
| 146 | + /// @param candidate The wETH to register. | |
| 147 | + function _ensureRegistered(IWETH candidate) private { | |
| 148 | + IWETH registered = registry.weth(); | |
| 149 | + | |
| 150 | + if (address(registered) != address(0)) { | |
| 151 | + console.log("weth already registered", address(registered)); | |
| 152 | + require( | |
| 153 | + address(registered) == address(candidate) || address(candidate) == DEFAULT_WETH, | |
| 154 | + "LoveScript: a different weth is registered on this chain" | |
| 155 | + ); | |
| 156 | + return; | |
| 157 | + } | |
| 158 | + | |
| 159 | + vm.startBroadcast(); | |
| 160 | + registry.register{value: registry.PROBE()}(candidate); | |
| 161 | + vm.stopBroadcast(); | |
| 162 | + | |
| 163 | + console.log("weth registered", address(registry.weth())); | |
| 164 | + } | |
| 165 | + | |
| 166 | + /// @dev Deploys the token unless it is already there. | |
| 167 | + /// @param predicted Where it should land. | |
| 168 | + function _ensureLove(address predicted) private { | |
| 169 | + if (predicted.code.length > 0) { | |
| 170 | + console.log("already deployed, nothing to do"); | |
| 171 | + love = Love(predicted); | |
| 172 | + return; | |
| 173 | + } | |
| 174 | + | |
| 175 | + vm.startBroadcast(); | |
| 176 | + love = new Love{salt: salt()}(); | |
| 177 | + vm.stopBroadcast(); | |
| 178 | + | |
| 179 | + require(address(love) == predicted, "LoveScript: address mismatch"); | |
| 180 | + | |
| 181 | + console.log("Love deployed at", address(love)); | |
| 182 | + console.log("name ", love.name()); | |
| 183 | + console.log("symbol ", love.symbol()); | |
| 184 | + console.log("weth ", address(love.WETH())); | |
| 185 | + console.log("rate ", love.RATE()); | |
| 186 | + console.log("totalSupply ", love.totalSupply()); | |
| 187 | + } | |
| 112 | 188 | } |
| @@ -1,112 +1,188 @@ | |||
| 1 | // SPDX-License-Identifier: MIT | 1 | // SPDX-License-Identifier: MIT |
| 2 | pragma solidity ^0.8.30; | 2 | pragma solidity ^0.8.30; |
| 3 | 3 | ||
| 4 | +import {IWETH} from "../src/IWETH.sol"; | ||
| 4 | import {Love} from "../src/Love.sol"; | 5 | import {Love} from "../src/Love.sol"; |
| 5 | -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | 6 | +import {WETHRegistry} from "../src/WETHRegistry.sol"; |
| 6 | import {Script, console} from "forge-std/Script.sol"; | 7 | import {Script, console} from "forge-std/Script.sol"; |
| 7 | 8 | ||
| 8 | /// @title LoveScript | 9 | /// @title LoveScript |
| 9 | /// @author Julien Béranger | 10 | /// @author Julien Béranger |
| 10 | -/// @notice Deploys `Love` with CREATE2 through the canonical deterministic | 11 | +/// @notice Brings LOVE up on a chain, in the order the design requires: |
| 11 | -/// deployer. The address depends on the salt and the creation code, and | 12 | +/// registry first, wETH registered second, token third. |
| 12 | -/// the creation code embeds the wETH address — so the token only gets | 13 | +/// @dev Both contracts go through the canonical deterministic deployer and |
| 13 | -/// the same address on chains where wETH sits at the same address. Keep | 14 | +/// neither takes constructor arguments, so their creation code is |
| 14 | -/// `solc`, the optimizer settings and `bytecode_hash = "none"` as they | 15 | +/// identical on every chain and so are their addresses. That is the whole |
| 15 | -/// are in `foundry.toml`, or the address changes too. | 16 | +/// point — LOVE is one address everywhere, not one per wETH deployment. |
| 17 | +/// Keep `solc`, the optimizer settings and `bytecode_hash = "none"` as | ||
| 18 | +/// they are in `foundry.toml`, or both addresses move. | ||
| 16 | contract LoveScript is Script { | 19 | contract LoveScript is Script { |
| 17 | - /// @notice Salt used when `SALT` is not set in the environment. | 20 | + /// @notice Salt used for `Love` when `SALT` is not set in the environment. |
| 18 | bytes32 public constant DEFAULT_SALT = keccak256("LOVE"); | 21 | bytes32 public constant DEFAULT_SALT = keccak256("LOVE"); |
| 19 | 22 | ||
| 23 | + /// @notice Salt the registry is deployed with. Not configurable: `Love` | ||
| 24 | + /// has the resulting address compiled into it. | ||
| 25 | + bytes32 public constant REGISTRY_SALT = keccak256("LOVE.WETHRegistry"); | ||
| 26 | + | ||
| 20 | /// @notice wETH address used when `WETH` is not set in the environment. | 27 | /// @notice wETH address used when `WETH` is not set in the environment. |
| 21 | - /// @dev wETH on Base, Optimism and every other OP-Stack chain. | 28 | + /// @dev The OP Stack predeploy, which is where wETH sits on Optimism, |
| 29 | + /// Base, Mode, Zora, Lisk, World Chain, Unichain, Soneium and Ink. | ||
| 30 | + /// Chains outside that set need `WETH` set explicitly. | ||
| 22 | address public constant DEFAULT_WETH = 0x4200000000000000000000000000000000000006; | 31 | address public constant DEFAULT_WETH = 0x4200000000000000000000000000000000000006; |
| 23 | 32 | ||
| 33 | + /// @notice The registry, once `run()` has deployed or found it. | ||
| 34 | + WETHRegistry public registry; | ||
| 35 | + | ||
| 24 | /// @notice The token, once `run()` has deployed or found it. | 36 | /// @notice The token, once `run()` has deployed or found it. |
| 25 | Love public love; | 37 | Love public love; |
| 26 | 38 | ||
| 27 | - /// @notice Deploy `Love`, or return it untouched if it is already there. | 39 | + /// @notice Deploy the registry, register wETH and deploy the token, |
| 40 | + /// skipping whichever of those has already happened. | ||
| 28 | /// @dev Reads `SALT` and `WETH` from the environment, falling back to the | 41 | /// @dev Reads `SALT` and `WETH` from the environment, falling back to the |
| 29 | - /// defaults, and asserts the deployed address matches the prediction. | 42 | + /// defaults, and asserts both deployments match their predictions. |
| 30 | /// @return The deployed token. | 43 | /// @return The deployed token. |
| 31 | function run() public returns (Love) { | 44 | function run() public returns (Love) { |
| 32 | - bytes32 s = salt(); | ||
| 33 | address w = wethAddress(); | 45 | address w = wethAddress(); |
| 34 | - address predicted = predict(s, w); | 46 | + address predictedRegistry = registryAddress(); |
| 47 | + address predicted = predict(salt()); | ||
| 35 | 48 | ||
| 36 | console.log("deployer ", CREATE2_FACTORY); | 49 | console.log("deployer ", CREATE2_FACTORY); |
| 37 | - console.log("salt ", vm.toString(s)); | 50 | + console.log("registry ", predictedRegistry); |
| 38 | console.log("weth ", w); | 51 | console.log("weth ", w); |
| 39 | - console.log("initCodeHash ", vm.toString(initCodeHash(w))); | 52 | + console.log("salt ", vm.toString(salt())); |
| 53 | + console.log("initCodeHash ", vm.toString(initCodeHash())); | ||
| 40 | console.log("predicted ", predicted); | 54 | console.log("predicted ", predicted); |
| 41 | 55 | ||
| 42 | - if (predicted.code.length > 0) { | 56 | + _ensureRegistry(predictedRegistry); |
| 43 | - console.log("already deployed, nothing to do"); | 57 | + _ensureRegistered(IWETH(w)); |
| 44 | - love = Love(predicted); | 58 | + _ensureLove(predicted); |
| 45 | - return love; | ||
| 46 | - } | ||
| 47 | - | ||
| 48 | - vm.startBroadcast(); | ||
| 49 | - love = new Love{salt: s}(IERC20(w)); | ||
| 50 | - vm.stopBroadcast(); | ||
| 51 | - | ||
| 52 | - require(address(love) == predicted, "LoveScript: address mismatch"); | ||
| 53 | - | ||
| 54 | - console.log("Love deployed at", address(love)); | ||
| 55 | - console.log("name ", love.name()); | ||
| 56 | - console.log("symbol ", love.symbol()); | ||
| 57 | - console.log("weth ", address(love.WETH())); | ||
| 58 | - console.log("rate ", love.RATE()); | ||
| 59 | - console.log("totalSupply ", love.totalSupply()); | ||
| 60 | 59 | ||
| 61 | return love; | 60 | return love; |
| 62 | } | 61 | } |
| 63 | 62 | ||
| 64 | - /// @notice Print the address `run()` would deploy to, without broadcasting. | 63 | + /// @notice Print what `run()` would deploy, without broadcasting. |
| 65 | - /// @return predicted The address the current salt and wETH derive to. | 64 | + /// @return predicted The address the current salt derives to. |
| 66 | function predict() public view returns (address predicted) { | 65 | function predict() public view returns (address predicted) { |
| 67 | - bytes32 s = salt(); | 66 | + predicted = predict(salt()); |
| 68 | - address w = wethAddress(); | ||
| 69 | - predicted = predict(s, w); | ||
| 70 | 67 | ||
| 71 | - console.log("salt ", vm.toString(s)); | 68 | + console.log("registry ", registryAddress()); |
| 72 | - console.log("weth ", w); | 69 | + console.log("weth ", wethAddress()); |
| 73 | - console.log("initCodeHash ", vm.toString(initCodeHash(w))); | 70 | + console.log("salt ", vm.toString(salt())); |
| 71 | + console.log("initCodeHash ", vm.toString(initCodeHash())); | ||
| 74 | console.log("predicted ", predicted); | 72 | console.log("predicted ", predicted); |
| 75 | } | 73 | } |
| 76 | 74 | ||
| 77 | - /// @notice Derive the deployment address from a salt and a wETH address. | 75 | + /// @notice Derive the token's deployment address from a salt. |
| 76 | + /// @dev Takes no wETH argument, unlike the address derivation this | ||
| 77 | + /// replaces: wETH is no longer in the creation code, which is why the | ||
| 78 | + /// answer is the same on every chain. | ||
| 78 | /// @param s The CREATE2 salt. | 79 | /// @param s The CREATE2 salt. |
| 79 | - /// @param w The wETH the token would be pegged to. | ||
| 80 | /// @return The address `Love` would land at. | 80 | /// @return The address `Love` would land at. |
| 81 | - function predict(bytes32 s, address w) public pure returns (address) { | 81 | + function predict(bytes32 s) public pure returns (address) { |
| 82 | - return vm.computeCreate2Address(s, initCodeHash(w), CREATE2_FACTORY); | 82 | + return vm.computeCreate2Address(s, initCodeHash(), CREATE2_FACTORY); |
| 83 | + } | ||
| 84 | + | ||
| 85 | + /// @notice The address the registry lands at on every chain. | ||
| 86 | + /// @return The registry's CREATE2 address. | ||
| 87 | + function registryAddress() public pure returns (address) { | ||
| 88 | + return vm.computeCreate2Address(REGISTRY_SALT, keccak256(registryInitCode()), CREATE2_FACTORY); | ||
| 83 | } | 89 | } |
| 84 | 90 | ||
| 85 | - /// @notice The creation code CREATE2 is handed, constructor argument included. | 91 | + /// @notice The token's creation code, which now carries no arguments. |
| 86 | - /// @param w The wETH the token would be pegged to. | 92 | + /// @return The creation code handed to CREATE2. |
| 87 | - /// @return The creation code, with `w` ABI-encoded onto it. | 93 | + function initCode() public pure returns (bytes memory) { |
| 88 | - function initCode(address w) public pure returns (bytes memory) { | 94 | + return type(Love).creationCode; |
| 89 | - return abi.encodePacked(type(Love).creationCode, abi.encode(w)); | ||
| 90 | } | 95 | } |
| 91 | 96 | ||
| 92 | - /// @notice Hash of the creation code, the second input to the address derivation. | 97 | + /// @notice Hash of the token's creation code. |
| 93 | - /// @param w The wETH the token would be pegged to. | 98 | + /// @return The keccak256 of `initCode()`. |
| 94 | - /// @return The keccak256 of `initCode(w)`. | 99 | + function initCodeHash() public pure returns (bytes32) { |
| 95 | - function initCodeHash(address w) public pure returns (bytes32) { | 100 | + return keccak256(initCode()); |
| 96 | - return keccak256(initCode(w)); | ||
| 97 | } | 101 | } |
| 98 | 102 | ||
| 99 | - /// @notice The salt to deploy with. | 103 | + /// @notice The registry's creation code. |
| 100 | - /// @dev `SALT` overrides the default, e.g. to mine a vanity address. | 104 | + /// @return The creation code handed to CREATE2. |
| 105 | + function registryInitCode() public pure returns (bytes memory) { | ||
| 106 | + return type(WETHRegistry).creationCode; | ||
| 107 | + } | ||
| 108 | + | ||
| 109 | + /// @notice The salt to deploy the token with. | ||
| 110 | + /// @dev `SALT` overrides the default, e.g. to mine a vanity address or to | ||
| 111 | + /// route around a chain where the registry has been squatted. | ||
| 101 | /// @return The configured salt, or `DEFAULT_SALT`. | 112 | /// @return The configured salt, or `DEFAULT_SALT`. |
| 102 | function salt() public view returns (bytes32) { | 113 | function salt() public view returns (bytes32) { |
| 103 | return vm.envOr("SALT", DEFAULT_SALT); | 114 | return vm.envOr("SALT", DEFAULT_SALT); |
| 104 | } | 115 | } |
| 105 | 116 | ||
| 106 | - /// @notice The wETH address to peg the deployment to. | 117 | + /// @notice The wETH address to register on this chain. |
| 107 | - /// @dev `WETH` overrides the default, which only holds on OP-Stack chains. | 118 | + /// @dev `WETH` overrides the default, which only holds on OP Stack chains. |
| 119 | + /// Ignored once a wETH is already registered. | ||
| 108 | /// @return The configured wETH address, or `DEFAULT_WETH`. | 120 | /// @return The configured wETH address, or `DEFAULT_WETH`. |
| 109 | function wethAddress() public view returns (address) { | 121 | function wethAddress() public view returns (address) { |
| 110 | return vm.envOr("WETH", DEFAULT_WETH); | 122 | return vm.envOr("WETH", DEFAULT_WETH); |
| 111 | } | 123 | } |
| 124 | + | ||
| 125 | + /// @dev Deploys the registry unless it is already there. | ||
| 126 | + /// @param predicted Where it should land. | ||
| 127 | + function _ensureRegistry(address predicted) private { | ||
| 128 | + if (predicted.code.length > 0) { | ||
| 129 | + console.log("registry already deployed"); | ||
| 130 | + registry = WETHRegistry(payable(predicted)); | ||
| 131 | + return; | ||
| 132 | + } | ||
| 133 | + | ||
| 134 | + vm.startBroadcast(); | ||
| 135 | + registry = new WETHRegistry{salt: REGISTRY_SALT}(); | ||
| 136 | + vm.stopBroadcast(); | ||
| 137 | + | ||
| 138 | + require(address(registry) == predicted, "LoveScript: registry address mismatch"); | ||
| 139 | + console.log("registry deployed at", address(registry)); | ||
| 140 | + } | ||
| 141 | + | ||
| 142 | + /// @dev Registers `candidate` unless a wETH is already registered. Reverts | ||
| 143 | + /// inside the registry if the candidate's code is not a reviewed wETH | ||
| 144 | + /// — which is the intended outcome on a chain whose wETH has not been | ||
| 145 | + /// reviewed, not a script failure to work around. | ||
| 146 | + /// @param candidate The wETH to register. | ||
| 147 | + function _ensureRegistered(IWETH candidate) private { | ||
| 148 | + IWETH registered = registry.weth(); | ||
| 149 | + | ||
| 150 | + if (address(registered) != address(0)) { | ||
| 151 | + console.log("weth already registered", address(registered)); | ||
| 152 | + require( | ||
| 153 | + address(registered) == address(candidate) || address(candidate) == DEFAULT_WETH, | ||
| 154 | + "LoveScript: a different weth is registered on this chain" | ||
| 155 | + ); | ||
| 156 | + return; | ||
| 157 | + } | ||
| 158 | + | ||
| 159 | + vm.startBroadcast(); | ||
| 160 | + registry.register{value: registry.PROBE()}(candidate); | ||
| 161 | + vm.stopBroadcast(); | ||
| 162 | + | ||
| 163 | + console.log("weth registered", address(registry.weth())); | ||
| 164 | + } | ||
| 165 | + | ||
| 166 | + /// @dev Deploys the token unless it is already there. | ||
| 167 | + /// @param predicted Where it should land. | ||
| 168 | + function _ensureLove(address predicted) private { | ||
| 169 | + if (predicted.code.length > 0) { | ||
| 170 | + console.log("already deployed, nothing to do"); | ||
| 171 | + love = Love(predicted); | ||
| 172 | + return; | ||
| 173 | + } | ||
| 174 | + | ||
| 175 | + vm.startBroadcast(); | ||
| 176 | + love = new Love{salt: salt()}(); | ||
| 177 | + vm.stopBroadcast(); | ||
| 178 | + | ||
| 179 | + require(address(love) == predicted, "LoveScript: address mismatch"); | ||
| 180 | + | ||
| 181 | + console.log("Love deployed at", address(love)); | ||
| 182 | + console.log("name ", love.name()); | ||
| 183 | + console.log("symbol ", love.symbol()); | ||
| 184 | + console.log("weth ", address(love.WETH())); | ||
| 185 | + console.log("rate ", love.RATE()); | ||
| 186 | + console.log("totalSupply ", love.totalSupply()); | ||
| 187 | + } | ||
| 112 | } | 188 | } |
modified
src/Love.sol +42 -5 | @@ -1,6 +1,8 @@ | ||
| 1 | 1 | // SPDX-License-Identifier: MIT |
| 2 | 2 | pragma solidity ^0.8.30; |
| 3 | 3 | |
| 4 | +import {IWETH} from "./IWETH.sol"; | |
| 5 | +import {WETHRegistry} from "./WETHRegistry.sol"; | |
| 4 | 6 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; |
| 5 | 7 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
| 6 | 8 | import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; |
| @@ -21,9 +23,23 @@ contract Love is ERC20 { | ||
| 21 | 23 | /// @notice LOVE minted per unit of wETH, and burned per unit released. |
| 22 | 24 | uint256 public constant RATE = 100_000; |
| 23 | 25 | |
| 26 | + /// @notice The registry this token asks which wETH to peg to. | |
| 27 | + /// @dev `WETHRegistry` takes no constructor arguments, so CREATE2 puts it | |
| 28 | + /// at this address on every chain. Asking it at construction time, | |
| 29 | + /// rather than taking wETH as a constructor argument, is what keeps | |
| 30 | + /// this contract's creation code byte-identical everywhere — and so | |
| 31 | + /// what gives LOVE one address on every chain instead of one per wETH | |
| 32 | + /// deployment. | |
| 33 | + /// | |
| 34 | + /// Derived from the registry's creation code, which means it moves if | |
| 35 | + /// `WETHRegistry`, the solc version or the optimizer settings change. | |
| 36 | + /// `LoveRegistryAddressTest` recomputes it and fails if this constant | |
| 37 | + /// has drifted. | |
| 38 | + address public constant REGISTRY = 0x9Cf17430fEdEC487518416D1Cdc849b1eE9CDbA3; | |
| 39 | + | |
| 24 | 40 | /// @notice The wETH this token is pegged to and collateralised with. |
| 25 | - /// @dev Immutable, and part of the creation code — two chains only give this | |
| 26 | - /// contract the same CREATE2 address if they share a wETH address. | |
| 41 | + /// @dev Read once from the registry and immutable thereafter. The registry | |
| 42 | + /// is itself write-once, so nothing can move the token under the peg. | |
| 27 | 43 | IERC20 public immutable WETH; |
| 28 | 44 | |
| 29 | 45 | /// @notice Thrown when a withdrawal amount is not a multiple of `RATE`. |
| @@ -31,6 +47,14 @@ contract Love is ERC20 { | ||
| 31 | 47 | /// @param rate The rate it has to be a multiple of. |
| 32 | 48 | error AmountNotDivisibleByRate(uint256 loveAmount, uint256 rate); |
| 33 | 49 | |
| 50 | + /// @notice Thrown when there is no registry on this chain yet. | |
| 51 | + /// @param registry The address the registry would be at. | |
| 52 | + error RegistryNotDeployed(address registry); | |
| 53 | + | |
| 54 | + /// @notice Thrown when the registry exists but holds no wETH yet. | |
| 55 | + /// @param registry The registry that was asked. | |
| 56 | + error WethNotRegistered(address registry); | |
| 57 | + | |
| 34 | 58 | /// @notice Emitted when wETH is locked and LOVE minted. |
| 35 | 59 | /// @param account The depositor, who pays the wETH and receives the LOVE. |
| 36 | 60 | /// @param wethAmount The wETH pulled in. |
| @@ -43,9 +67,22 @@ contract Love is ERC20 { | ||
| 43 | 67 | /// @param wethAmount The wETH released, `loveAmount / RATE`. |
| 44 | 68 | event Withdraw(address indexed account, uint256 loveAmount, uint256 wethAmount); |
| 45 | 69 | |
| 46 | - /// @param weth_ The wETH to peg to. Set once, never changed. | |
| 47 | - constructor(IERC20 weth_) ERC20("Love", "LOVE") { | |
| 48 | - WETH = weth_; | |
| 70 | + /// @notice Peg to whatever wETH the registry has accepted on this chain. | |
| 71 | + /// @dev Takes no arguments on purpose. Anything passed in here would land | |
| 72 | + /// in the creation code and give the token a different address on | |
| 73 | + /// every chain whose wETH sits elsewhere, which is exactly what this | |
| 74 | + /// design exists to avoid. | |
| 75 | + /// | |
| 76 | + /// Reverts when the registry is missing or empty, so a chain with no | |
| 77 | + /// reviewed wETH gets no half-configured token: deploy the registry | |
| 78 | + /// and register wETH first, then deploy this. | |
| 79 | + constructor() ERC20("Love", "LOVE") { | |
| 80 | + if (REGISTRY.code.length == 0) revert RegistryNotDeployed(REGISTRY); | |
| 81 | + | |
| 82 | + IWETH registered = WETHRegistry(payable(REGISTRY)).weth(); | |
| 83 | + if (address(registered) == address(0)) revert WethNotRegistered(REGISTRY); | |
| 84 | + | |
| 85 | + WETH = IERC20(address(registered)); | |
| 49 | 86 | } |
| 50 | 87 | |
| 51 | 88 | /// @notice Lock `wethAmount` wETH and mint `wethAmount * RATE` LOVE to the caller. |
| @@ -1,6 +1,8 @@ | |||
| 1 | // SPDX-License-Identifier: MIT | 1 | // SPDX-License-Identifier: MIT |
| 2 | pragma solidity ^0.8.30; | 2 | pragma solidity ^0.8.30; |
| 3 | 3 | ||
| 4 | +import {IWETH} from "./IWETH.sol"; | ||
| 5 | +import {WETHRegistry} from "./WETHRegistry.sol"; | ||
| 4 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; | 6 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; |
| 5 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | 7 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
| 6 | import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | 8 | import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; |
| @@ -21,9 +23,23 @@ contract Love is ERC20 { | |||
| 21 | /// @notice LOVE minted per unit of wETH, and burned per unit released. | 23 | /// @notice LOVE minted per unit of wETH, and burned per unit released. |
| 22 | uint256 public constant RATE = 100_000; | 24 | uint256 public constant RATE = 100_000; |
| 23 | 25 | ||
| 26 | + /// @notice The registry this token asks which wETH to peg to. | ||
| 27 | + /// @dev `WETHRegistry` takes no constructor arguments, so CREATE2 puts it | ||
| 28 | + /// at this address on every chain. Asking it at construction time, | ||
| 29 | + /// rather than taking wETH as a constructor argument, is what keeps | ||
| 30 | + /// this contract's creation code byte-identical everywhere — and so | ||
| 31 | + /// what gives LOVE one address on every chain instead of one per wETH | ||
| 32 | + /// deployment. | ||
| 33 | + /// | ||
| 34 | + /// Derived from the registry's creation code, which means it moves if | ||
| 35 | + /// `WETHRegistry`, the solc version or the optimizer settings change. | ||
| 36 | + /// `LoveRegistryAddressTest` recomputes it and fails if this constant | ||
| 37 | + /// has drifted. | ||
| 38 | + address public constant REGISTRY = 0x9Cf17430fEdEC487518416D1Cdc849b1eE9CDbA3; | ||
| 39 | + | ||
| 24 | /// @notice The wETH this token is pegged to and collateralised with. | 40 | /// @notice The wETH this token is pegged to and collateralised with. |
| 25 | - /// @dev Immutable, and part of the creation code — two chains only give this | 41 | + /// @dev Read once from the registry and immutable thereafter. The registry |
| 26 | - /// contract the same CREATE2 address if they share a wETH address. | 42 | + /// is itself write-once, so nothing can move the token under the peg. |
| 27 | IERC20 public immutable WETH; | 43 | IERC20 public immutable WETH; |
| 28 | 44 | ||
| 29 | /// @notice Thrown when a withdrawal amount is not a multiple of `RATE`. | 45 | /// @notice Thrown when a withdrawal amount is not a multiple of `RATE`. |
| @@ -31,6 +47,14 @@ contract Love is ERC20 { | |||
| 31 | /// @param rate The rate it has to be a multiple of. | 47 | /// @param rate The rate it has to be a multiple of. |
| 32 | error AmountNotDivisibleByRate(uint256 loveAmount, uint256 rate); | 48 | error AmountNotDivisibleByRate(uint256 loveAmount, uint256 rate); |
| 33 | 49 | ||
| 50 | + /// @notice Thrown when there is no registry on this chain yet. | ||
| 51 | + /// @param registry The address the registry would be at. | ||
| 52 | + error RegistryNotDeployed(address registry); | ||
| 53 | + | ||
| 54 | + /// @notice Thrown when the registry exists but holds no wETH yet. | ||
| 55 | + /// @param registry The registry that was asked. | ||
| 56 | + error WethNotRegistered(address registry); | ||
| 57 | + | ||
| 34 | /// @notice Emitted when wETH is locked and LOVE minted. | 58 | /// @notice Emitted when wETH is locked and LOVE minted. |
| 35 | /// @param account The depositor, who pays the wETH and receives the LOVE. | 59 | /// @param account The depositor, who pays the wETH and receives the LOVE. |
| 36 | /// @param wethAmount The wETH pulled in. | 60 | /// @param wethAmount The wETH pulled in. |
| @@ -43,9 +67,22 @@ contract Love is ERC20 { | |||
| 43 | /// @param wethAmount The wETH released, `loveAmount / RATE`. | 67 | /// @param wethAmount The wETH released, `loveAmount / RATE`. |
| 44 | event Withdraw(address indexed account, uint256 loveAmount, uint256 wethAmount); | 68 | event Withdraw(address indexed account, uint256 loveAmount, uint256 wethAmount); |
| 45 | 69 | ||
| 46 | - /// @param weth_ The wETH to peg to. Set once, never changed. | 70 | + /// @notice Peg to whatever wETH the registry has accepted on this chain. |
| 47 | - constructor(IERC20 weth_) ERC20("Love", "LOVE") { | 71 | + /// @dev Takes no arguments on purpose. Anything passed in here would land |
| 48 | - WETH = weth_; | 72 | + /// in the creation code and give the token a different address on |
| 73 | + /// every chain whose wETH sits elsewhere, which is exactly what this | ||
| 74 | + /// design exists to avoid. | ||
| 75 | + /// | ||
| 76 | + /// Reverts when the registry is missing or empty, so a chain with no | ||
| 77 | + /// reviewed wETH gets no half-configured token: deploy the registry | ||
| 78 | + /// and register wETH first, then deploy this. | ||
| 79 | + constructor() ERC20("Love", "LOVE") { | ||
| 80 | + if (REGISTRY.code.length == 0) revert RegistryNotDeployed(REGISTRY); | ||
| 81 | + | ||
| 82 | + IWETH registered = WETHRegistry(payable(REGISTRY)).weth(); | ||
| 83 | + if (address(registered) == address(0)) revert WethNotRegistered(REGISTRY); | ||
| 84 | + | ||
| 85 | + WETH = IERC20(address(registered)); | ||
| 49 | } | 86 | } |
| 50 | 87 | ||
| 51 | /// @notice Lock `wethAmount` wETH and mint `wethAmount * RATE` LOVE to the caller. | 88 | /// @notice Lock `wethAmount` wETH and mint `wethAmount * RATE` LOVE to the caller. |
modified
src/WETHRegistry.sol +8 -9 | @@ -49,6 +49,7 @@ contract WETHRegistry { | ||
| 49 | 49 | IWETH public weth; |
| 50 | 50 | |
| 51 | 51 | /// @notice The ether moved through the candidate to prove it wraps. |
| 52 | + /// @dev Stays in the registry afterwards; see `register`. | |
| 52 | 53 | uint256 public constant PROBE = 1 wei; |
| 53 | 54 | |
| 54 | 55 | /// @dev Open only for the duration of the round trip, so the registry |
| @@ -87,9 +88,6 @@ contract WETHRegistry { | ||
| 87 | 88 | /// @notice Thrown when ether is sent outside a round trip. |
| 88 | 89 | error NotProbing(); |
| 89 | 90 | |
| 90 | - /// @notice Thrown when the probe wei could not be sent back. | |
| 91 | - error RefundFailed(); | |
| 92 | - | |
| 93 | 91 | /// @notice Emitted once, when a chain's wETH is settled. |
| 94 | 92 | /// @param weth The accepted wETH. |
| 95 | 93 | /// @param registrar Whoever supplied and paid for it. |
| @@ -98,7 +96,13 @@ contract WETHRegistry { | ||
| 98 | 96 | |
| 99 | 97 | /// @notice Accept `candidate` as this chain's wETH, if its code is one of |
| 100 | 98 | /// the reviewed implementations and it wraps ether correctly. |
| 101 | - /// @dev Send exactly `PROBE` wei; it makes the round trip and comes back. | |
| 99 | + /// @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 | + /// | |
| 102 | 106 | /// Reverts rather than degrading when the candidate is unrecognised — |
| 103 | 107 | /// unreviewed bytecode cannot be shown safe by any test, static or |
| 104 | 108 | /// behavioural, so a chain running its own wETH is a chain `Love` |
| @@ -118,11 +122,6 @@ contract WETHRegistry { | ||
| 118 | 122 | weth = candidate; |
| 119 | 123 | emit Registered(candidate, msg.sender, codeHash); |
| 120 | 124 | |
| 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 | 125 | return candidate; |
| 127 | 126 | } |
| 128 | 127 | |
| @@ -49,6 +49,7 @@ contract WETHRegistry { | |||
| 49 | IWETH public weth; | 49 | IWETH public weth; |
| 50 | 50 | ||
| 51 | /// @notice The ether moved through the candidate to prove it wraps. | 51 | /// @notice The ether moved through the candidate to prove it wraps. |
| 52 | + /// @dev Stays in the registry afterwards; see `register`. | ||
| 52 | uint256 public constant PROBE = 1 wei; | 53 | uint256 public constant PROBE = 1 wei; |
| 53 | 54 | ||
| 54 | /// @dev Open only for the duration of the round trip, so the registry | 55 | /// @dev Open only for the duration of the round trip, so the registry |
| @@ -87,9 +88,6 @@ contract WETHRegistry { | |||
| 87 | /// @notice Thrown when ether is sent outside a round trip. | 88 | /// @notice Thrown when ether is sent outside a round trip. |
| 88 | error NotProbing(); | 89 | error NotProbing(); |
| 89 | 90 | ||
| 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. | 91 | /// @notice Emitted once, when a chain's wETH is settled. |
| 94 | /// @param weth The accepted wETH. | 92 | /// @param weth The accepted wETH. |
| 95 | /// @param registrar Whoever supplied and paid for it. | 93 | /// @param registrar Whoever supplied and paid for it. |
| @@ -98,7 +96,13 @@ contract WETHRegistry { | |||
| 98 | 96 | ||
| 99 | /// @notice Accept `candidate` as this chain's wETH, if its code is one of | 97 | /// @notice Accept `candidate` as this chain's wETH, if its code is one of |
| 100 | /// the reviewed implementations and it wraps ether correctly. | 98 | /// the reviewed implementations and it wraps ether correctly. |
| 101 | - /// @dev Send exactly `PROBE` wei; it makes the round trip and comes back. | 99 | + /// @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 | + /// | ||
| 102 | /// Reverts rather than degrading when the candidate is unrecognised — | 106 | /// Reverts rather than degrading when the candidate is unrecognised — |
| 103 | /// unreviewed bytecode cannot be shown safe by any test, static or | 107 | /// unreviewed bytecode cannot be shown safe by any test, static or |
| 104 | /// behavioural, so a chain running its own wETH is a chain `Love` | 108 | /// behavioural, so a chain running its own wETH is a chain `Love` |
| @@ -118,11 +122,6 @@ contract WETHRegistry { | |||
| 118 | weth = candidate; | 122 | weth = candidate; |
| 119 | emit Registered(candidate, msg.sender, codeHash); | 123 | emit Registered(candidate, msg.sender, codeHash); |
| 120 | 124 | ||
| 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; | 125 | return candidate; |
| 127 | } | 126 | } |
| 128 | 127 | ||
added
test/Fixtures.sol +78 -0 | new file mode 100644 | ||
| @@ -0,0 +1,78 @@ | ||
| 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 {CommonBase} from "forge-std/Base.sol"; | |
| 7 | +import {StdCheats} from "forge-std/StdCheats.sol"; | |
| 8 | + | |
| 9 | +/// @title Fixtures | |
| 10 | +/// @author Julien Béranger | |
| 11 | +/// @notice Brings a chain up the way a real one comes up: the registry at its | |
| 12 | +/// deterministic address, a genuine wETH at some address, registered. | |
| 13 | +/// @dev The wETH here is real runtime bytecode lifted off a live chain, etched | |
| 14 | +/// into place. A mock would need its codehash added to the allowlist to | |
| 15 | +/// be registrable, which would mean testing a different allowlist from | |
| 16 | +/// the one that ships. Etching real code keeps the test and the product | |
| 17 | +/// honest about the same ten hashes. | |
| 18 | +abstract contract Fixtures is CommonBase, StdCheats { | |
| 19 | + /// @notice Salt the registry is deployed with, here and in the script. | |
| 20 | + bytes32 internal constant REGISTRY_SALT = keccak256("LOVE.WETHRegistry"); | |
| 21 | + | |
| 22 | + /// @notice OP Stack legacy WETH9, as deployed on Base, Mode and Zora. | |
| 23 | + string internal constant WETH9_OP_LEGACY = "test/fixtures/weth9-op-legacy.hex"; | |
| 24 | + | |
| 25 | + /// @notice Canonical WETH9, as deployed on Ethereum mainnet. | |
| 26 | + string internal constant WETH9_CANONICAL = "test/fixtures/weth9-canonical.hex"; | |
| 27 | + | |
| 28 | + /// @notice Put real wETH runtime bytecode at `where`. | |
| 29 | + /// @dev Reads the fixture rather than embedding it, so refreshing a | |
| 30 | + /// fixture from chain state does not mean editing Solidity. | |
| 31 | + /// @param where The address to place it at. | |
| 32 | + /// @param fixture Path to the runtime bytecode, `WETH9_*` above. | |
| 33 | + /// @return The wETH now living at `where`. | |
| 34 | + function etchWeth(address where, string memory fixture) internal returns (IWETH) { | |
| 35 | + vm.etch(where, vm.parseBytes(vm.trim(vm.readFile(fixture)))); | |
| 36 | + vm.label(where, "WETH"); | |
| 37 | + return IWETH(where); | |
| 38 | + } | |
| 39 | + | |
| 40 | + /// @notice Deploy the registry to the address `Love` expects. | |
| 41 | + /// @dev Through the canonical deterministic deployer with `REGISTRY_SALT`, | |
| 42 | + /// exactly as the deploy script does, so the address it lands at is | |
| 43 | + /// the one compiled into `Love.REGISTRY` rather than one arranged for | |
| 44 | + /// the test. | |
| 45 | + /// @return The registry. | |
| 46 | + function deployRegistry() internal returns (WETHRegistry) { | |
| 47 | + (bool ok, bytes memory ret) = | |
| 48 | + CREATE2_FACTORY.call(abi.encodePacked(REGISTRY_SALT, type(WETHRegistry).creationCode)); | |
| 49 | + require(ok, "registry deployment failed"); | |
| 50 | + | |
| 51 | + // casting to 'bytes20' is safe because the deployer returns the 20-byte address, raw | |
| 52 | + // forge-lint: disable-next-line(unsafe-typecast) | |
| 53 | + WETHRegistry registry = WETHRegistry(payable(address(bytes20(ret)))); | |
| 54 | + vm.label(address(registry), "WETHRegistry"); | |
| 55 | + | |
| 56 | + return registry; | |
| 57 | + } | |
| 58 | + | |
| 59 | + /// @notice Register `weth`, funding the one-wei probe. | |
| 60 | + /// @param registry The registry to register with. | |
| 61 | + /// @param weth The wETH to register. | |
| 62 | + function register(WETHRegistry registry, IWETH weth) internal { | |
| 63 | + uint256 probe = registry.PROBE(); | |
| 64 | + deal(address(this), address(this).balance + probe); | |
| 65 | + registry.register{value: probe}(weth); | |
| 66 | + } | |
| 67 | + | |
| 68 | + /// @notice The whole chain in one call: registry, wETH, registration. | |
| 69 | + /// @param wethAt The address to etch wETH at. | |
| 70 | + /// @param fixture Which implementation to etch. | |
| 71 | + /// @return registry The registry, with `wethAt` registered. | |
| 72 | + /// @return weth The wETH now backing any `Love` deployed here. | |
| 73 | + function setUpChain(address wethAt, string memory fixture) internal returns (WETHRegistry registry, IWETH weth) { | |
| 74 | + registry = deployRegistry(); | |
| 75 | + weth = etchWeth(wethAt, fixture); | |
| 76 | + register(registry, weth); | |
| 77 | + } | |
| 78 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,78 @@ | |||
| 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 {CommonBase} from "forge-std/Base.sol"; | ||
| 7 | +import {StdCheats} from "forge-std/StdCheats.sol"; | ||
| 8 | + | ||
| 9 | +/// @title Fixtures | ||
| 10 | +/// @author Julien Béranger | ||
| 11 | +/// @notice Brings a chain up the way a real one comes up: the registry at its | ||
| 12 | +/// deterministic address, a genuine wETH at some address, registered. | ||
| 13 | +/// @dev The wETH here is real runtime bytecode lifted off a live chain, etched | ||
| 14 | +/// into place. A mock would need its codehash added to the allowlist to | ||
| 15 | +/// be registrable, which would mean testing a different allowlist from | ||
| 16 | +/// the one that ships. Etching real code keeps the test and the product | ||
| 17 | +/// honest about the same ten hashes. | ||
| 18 | +abstract contract Fixtures is CommonBase, StdCheats { | ||
| 19 | + /// @notice Salt the registry is deployed with, here and in the script. | ||
| 20 | + bytes32 internal constant REGISTRY_SALT = keccak256("LOVE.WETHRegistry"); | ||
| 21 | + | ||
| 22 | + /// @notice OP Stack legacy WETH9, as deployed on Base, Mode and Zora. | ||
| 23 | + string internal constant WETH9_OP_LEGACY = "test/fixtures/weth9-op-legacy.hex"; | ||
| 24 | + | ||
| 25 | + /// @notice Canonical WETH9, as deployed on Ethereum mainnet. | ||
| 26 | + string internal constant WETH9_CANONICAL = "test/fixtures/weth9-canonical.hex"; | ||
| 27 | + | ||
| 28 | + /// @notice Put real wETH runtime bytecode at `where`. | ||
| 29 | + /// @dev Reads the fixture rather than embedding it, so refreshing a | ||
| 30 | + /// fixture from chain state does not mean editing Solidity. | ||
| 31 | + /// @param where The address to place it at. | ||
| 32 | + /// @param fixture Path to the runtime bytecode, `WETH9_*` above. | ||
| 33 | + /// @return The wETH now living at `where`. | ||
| 34 | + function etchWeth(address where, string memory fixture) internal returns (IWETH) { | ||
| 35 | + vm.etch(where, vm.parseBytes(vm.trim(vm.readFile(fixture)))); | ||
| 36 | + vm.label(where, "WETH"); | ||
| 37 | + return IWETH(where); | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + /// @notice Deploy the registry to the address `Love` expects. | ||
| 41 | + /// @dev Through the canonical deterministic deployer with `REGISTRY_SALT`, | ||
| 42 | + /// exactly as the deploy script does, so the address it lands at is | ||
| 43 | + /// the one compiled into `Love.REGISTRY` rather than one arranged for | ||
| 44 | + /// the test. | ||
| 45 | + /// @return The registry. | ||
| 46 | + function deployRegistry() internal returns (WETHRegistry) { | ||
| 47 | + (bool ok, bytes memory ret) = | ||
| 48 | + CREATE2_FACTORY.call(abi.encodePacked(REGISTRY_SALT, type(WETHRegistry).creationCode)); | ||
| 49 | + require(ok, "registry deployment failed"); | ||
| 50 | + | ||
| 51 | + // casting to 'bytes20' is safe because the deployer returns the 20-byte address, raw | ||
| 52 | + // forge-lint: disable-next-line(unsafe-typecast) | ||
| 53 | + WETHRegistry registry = WETHRegistry(payable(address(bytes20(ret)))); | ||
| 54 | + vm.label(address(registry), "WETHRegistry"); | ||
| 55 | + | ||
| 56 | + return registry; | ||
| 57 | + } | ||
| 58 | + | ||
| 59 | + /// @notice Register `weth`, funding the one-wei probe. | ||
| 60 | + /// @param registry The registry to register with. | ||
| 61 | + /// @param weth The wETH to register. | ||
| 62 | + function register(WETHRegistry registry, IWETH weth) internal { | ||
| 63 | + uint256 probe = registry.PROBE(); | ||
| 64 | + deal(address(this), address(this).balance + probe); | ||
| 65 | + registry.register{value: probe}(weth); | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + /// @notice The whole chain in one call: registry, wETH, registration. | ||
| 69 | + /// @param wethAt The address to etch wETH at. | ||
| 70 | + /// @param fixture Which implementation to etch. | ||
| 71 | + /// @return registry The registry, with `wethAt` registered. | ||
| 72 | + /// @return weth The wETH now backing any `Love` deployed here. | ||
| 73 | + function setUpChain(address wethAt, string memory fixture) internal returns (WETHRegistry registry, IWETH weth) { | ||
| 74 | + registry = deployRegistry(); | ||
| 75 | + weth = etchWeth(wethAt, fixture); | ||
| 76 | + register(registry, weth); | ||
| 77 | + } | ||
| 78 | +} | ||
modified
test/Love.t.sol +18 -13 | @@ -1,22 +1,23 @@ | ||
| 1 | 1 | // SPDX-License-Identifier: MIT |
| 2 | 2 | pragma solidity ^0.8.30; |
| 3 | 3 | |
| 4 | +import {IWETH} from "../src/IWETH.sol"; | |
| 4 | 5 | import {Love} from "../src/Love.sol"; |
| 5 | -import {MockWETH} from "./mocks/MockWETH.sol"; | |
| 6 | +import {Fixtures} from "./Fixtures.sol"; | |
| 6 | 7 | import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; |
| 7 | 8 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
| 8 | 9 | import {Test} from "forge-std/Test.sol"; |
| 9 | 10 | |
| 10 | 11 | /// @title LoveTest |
| 11 | -/// @notice Exercises the peg end to end against a WETH9 stand-in: minting on | |
| 12 | +/// @notice Exercises the peg end to end against real WETH9 bytecode: minting on | |
| 12 | 13 | /// deposit, redemption on withdraw, the divisibility rule, and the |
| 13 | 14 | /// invariant that supply always equals the wETH backing times the rate. |
| 14 | -contract LoveTest is Test { | |
| 15 | +contract LoveTest is Test, Fixtures { | |
| 15 | 16 | /// @notice The token under test. |
| 16 | 17 | Love public love; |
| 17 | 18 | |
| 18 | - /// @notice The wETH it is pegged to. | |
| 19 | - MockWETH public weth; | |
| 19 | + /// @notice The wETH it is pegged to, etched from a live chain. | |
| 20 | + IWETH public weth; | |
| 20 | 21 | |
| 21 | 22 | address alice = makeAddr("alice"); |
| 22 | 23 | address bob = makeAddr("bob"); |
| @@ -24,9 +25,12 @@ contract LoveTest is Test { | ||
| 24 | 25 | |
| 25 | 26 | uint256 constant RATE = 100_000; |
| 26 | 27 | |
| 28 | + /// @dev The OP Stack predeploy address, so the setup mirrors a real chain. | |
| 29 | + address constant WETH_AT = 0x4200000000000000000000000000000000000006; | |
| 30 | + | |
| 27 | 31 | function setUp() public { |
| 28 | - weth = new MockWETH(); | |
| 29 | - love = new Love(IERC20(address(weth))); | |
| 32 | + (, weth) = setUpChain(WETH_AT, WETH9_OP_LEGACY); | |
| 33 | + love = new Love(); | |
| 30 | 34 | |
| 31 | 35 | _fund(alice, 10 ether); |
| 32 | 36 | _fund(bob, 10 ether); |
| @@ -115,11 +119,14 @@ contract LoveTest is Test { | ||
| 115 | 119 | assertEq(love.totalSupply(), 0); |
| 116 | 120 | } |
| 117 | 121 | |
| 122 | + /// @dev WETH9 predates custom errors and guards `transferFrom` with a bare | |
| 123 | + /// `require`, so a shortfall comes back as a revert carrying no data | |
| 124 | + /// at all rather than as a typed ERC-20 error. Asserting the empty | |
| 125 | + /// data keeps that documented: anything integrating with LOVE has a | |
| 126 | + /// reason to fail, but not a machine-readable one. | |
| 118 | 127 | function test_RevertWhen_DepositWithoutApproval() public { |
| 119 | 128 | vm.prank(alice); |
| 120 | - vm.expectRevert( | |
| 121 | - abi.encodeWithSelector(IERC20Errors.ERC20InsufficientAllowance.selector, address(love), 0, 1 ether) | |
| 122 | - ); | |
| 129 | + vm.expectRevert(bytes("")); | |
| 123 | 130 | love.deposit(1 ether); |
| 124 | 131 | } |
| 125 | 132 | |
| @@ -128,9 +135,7 @@ contract LoveTest is Test { | ||
| 128 | 135 | weth.approve(address(love), 100 ether); |
| 129 | 136 | |
| 130 | 137 | vm.prank(alice); |
| 131 | - vm.expectRevert( | |
| 132 | - abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, alice, 10 ether, 100 ether) | |
| 133 | - ); | |
| 138 | + vm.expectRevert(bytes("")); | |
| 134 | 139 | love.deposit(100 ether); |
| 135 | 140 | } |
| 136 | 141 | |
| @@ -1,22 +1,23 @@ | |||
| 1 | // SPDX-License-Identifier: MIT | 1 | // SPDX-License-Identifier: MIT |
| 2 | pragma solidity ^0.8.30; | 2 | pragma solidity ^0.8.30; |
| 3 | 3 | ||
| 4 | +import {IWETH} from "../src/IWETH.sol"; | ||
| 4 | import {Love} from "../src/Love.sol"; | 5 | import {Love} from "../src/Love.sol"; |
| 5 | -import {MockWETH} from "./mocks/MockWETH.sol"; | 6 | +import {Fixtures} from "./Fixtures.sol"; |
| 6 | import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; | 7 | import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; |
| 7 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | 8 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
| 8 | import {Test} from "forge-std/Test.sol"; | 9 | import {Test} from "forge-std/Test.sol"; |
| 9 | 10 | ||
| 10 | /// @title LoveTest | 11 | /// @title LoveTest |
| 11 | -/// @notice Exercises the peg end to end against a WETH9 stand-in: minting on | 12 | +/// @notice Exercises the peg end to end against real WETH9 bytecode: minting on |
| 12 | /// deposit, redemption on withdraw, the divisibility rule, and the | 13 | /// deposit, redemption on withdraw, the divisibility rule, and the |
| 13 | /// invariant that supply always equals the wETH backing times the rate. | 14 | /// invariant that supply always equals the wETH backing times the rate. |
| 14 | -contract LoveTest is Test { | 15 | +contract LoveTest is Test, Fixtures { |
| 15 | /// @notice The token under test. | 16 | /// @notice The token under test. |
| 16 | Love public love; | 17 | Love public love; |
| 17 | 18 | ||
| 18 | - /// @notice The wETH it is pegged to. | 19 | + /// @notice The wETH it is pegged to, etched from a live chain. |
| 19 | - MockWETH public weth; | 20 | + IWETH public weth; |
| 20 | 21 | ||
| 21 | address alice = makeAddr("alice"); | 22 | address alice = makeAddr("alice"); |
| 22 | address bob = makeAddr("bob"); | 23 | address bob = makeAddr("bob"); |
| @@ -24,9 +25,12 @@ contract LoveTest is Test { | |||
| 24 | 25 | ||
| 25 | uint256 constant RATE = 100_000; | 26 | uint256 constant RATE = 100_000; |
| 26 | 27 | ||
| 28 | + /// @dev The OP Stack predeploy address, so the setup mirrors a real chain. | ||
| 29 | + address constant WETH_AT = 0x4200000000000000000000000000000000000006; | ||
| 30 | + | ||
| 27 | function setUp() public { | 31 | function setUp() public { |
| 28 | - weth = new MockWETH(); | 32 | + (, weth) = setUpChain(WETH_AT, WETH9_OP_LEGACY); |
| 29 | - love = new Love(IERC20(address(weth))); | 33 | + love = new Love(); |
| 30 | 34 | ||
| 31 | _fund(alice, 10 ether); | 35 | _fund(alice, 10 ether); |
| 32 | _fund(bob, 10 ether); | 36 | _fund(bob, 10 ether); |
| @@ -115,11 +119,14 @@ contract LoveTest is Test { | |||
| 115 | assertEq(love.totalSupply(), 0); | 119 | assertEq(love.totalSupply(), 0); |
| 116 | } | 120 | } |
| 117 | 121 | ||
| 122 | + /// @dev WETH9 predates custom errors and guards `transferFrom` with a bare | ||
| 123 | + /// `require`, so a shortfall comes back as a revert carrying no data | ||
| 124 | + /// at all rather than as a typed ERC-20 error. Asserting the empty | ||
| 125 | + /// data keeps that documented: anything integrating with LOVE has a | ||
| 126 | + /// reason to fail, but not a machine-readable one. | ||
| 118 | function test_RevertWhen_DepositWithoutApproval() public { | 127 | function test_RevertWhen_DepositWithoutApproval() public { |
| 119 | vm.prank(alice); | 128 | vm.prank(alice); |
| 120 | - vm.expectRevert( | 129 | + vm.expectRevert(bytes("")); |
| 121 | - abi.encodeWithSelector(IERC20Errors.ERC20InsufficientAllowance.selector, address(love), 0, 1 ether) | ||
| 122 | - ); | ||
| 123 | love.deposit(1 ether); | 130 | love.deposit(1 ether); |
| 124 | } | 131 | } |
| 125 | 132 | ||
| @@ -128,9 +135,7 @@ contract LoveTest is Test { | |||
| 128 | weth.approve(address(love), 100 ether); | 135 | weth.approve(address(love), 100 ether); |
| 129 | 136 | ||
| 130 | vm.prank(alice); | 137 | vm.prank(alice); |
| 131 | - vm.expectRevert( | 138 | + vm.expectRevert(bytes("")); |
| 132 | - abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, alice, 10 ether, 100 ether) | ||
| 133 | - ); | ||
| 134 | love.deposit(100 ether); | 139 | love.deposit(100 ether); |
| 135 | } | 140 | } |
| 136 | 141 | ||
modified
test/LoveCreate2.t.sol +110 -26 | @@ -3,16 +3,20 @@ pragma solidity ^0.8.30; | ||
| 3 | 3 | |
| 4 | 4 | import {LoveScript} from "../script/Love.s.sol"; |
| 5 | 5 | import {Love} from "../src/Love.sol"; |
| 6 | +import {Fixtures} from "./Fixtures.sol"; | |
| 6 | 7 | import {Test} from "forge-std/Test.sol"; |
| 7 | 8 | |
| 8 | 9 | /// @title LoveCreate2Test |
| 9 | -/// @notice The deployment address derives from the salt, the creation code and | |
| 10 | -/// the wETH constructor argument. These tests pin that derivation: same | |
| 11 | -/// salt and same wETH give the same address on any chain, a different | |
| 12 | -/// wETH gives a different one. | |
| 13 | -contract LoveCreate2Test is Test { | |
| 10 | +/// @notice The deployment address now derives from the salt and the creation | |
| 11 | +/// code alone. wETH used to be a constructor argument and so part of | |
| 12 | +/// that creation code, which gave the token a different address on | |
| 13 | +/// every chain whose wETH sat elsewhere; it is read from the registry | |
| 14 | +/// instead. These tests pin the consequence: same salt, same address, | |
| 15 | +/// on any chain, whatever its wETH is and wherever it lives. | |
| 16 | +contract LoveCreate2Test is Test, Fixtures { | |
| 14 | 17 | bytes32 constant SALT = keccak256("LOVE"); |
| 15 | - address constant WETH = 0x4200000000000000000000000000000000000006; | |
| 18 | + address constant OP_STACK_WETH = 0x4200000000000000000000000000000000000006; | |
| 19 | + address constant MAINNET_WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; | |
| 16 | 20 | |
| 17 | 21 | LoveScript script; |
| 18 | 22 | |
| @@ -23,19 +27,56 @@ contract LoveCreate2Test is Test { | ||
| 23 | 27 | assertGt(CREATE2_FACTORY.code.length, 0, "create2 deployer missing"); |
| 24 | 28 | } |
| 25 | 29 | |
| 30 | + /*////////////////////////////////////////////////////////////// | |
| 31 | + THE REGISTRY | |
| 32 | + //////////////////////////////////////////////////////////////*/ | |
| 33 | + | |
| 34 | + /// @dev `Love` has the registry's address compiled in, so if the registry's | |
| 35 | + /// creation code moves — an edit to the contract, a solc bump, a | |
| 36 | + /// change of optimizer settings — this fails rather than shipping a | |
| 37 | + /// token pointing at an empty address. | |
| 38 | + function test_LoveRegistryConstantMatchesDeterministicAddress() public { | |
| 39 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); | |
| 40 | + | |
| 41 | + assertEq(new Love().REGISTRY(), script.registryAddress()); | |
| 42 | + } | |
| 43 | + | |
| 44 | + function test_RegistryLandsAtTheAddressLoveExpects() public { | |
| 45 | + assertEq(address(deployRegistry()), script.registryAddress()); | |
| 46 | + } | |
| 47 | + | |
| 48 | + /// @dev The registry takes no constructor arguments, which is what makes | |
| 49 | + /// its address the same everywhere. | |
| 50 | + function test_RegistryAddressIsChainAgnostic() public { | |
| 51 | + address onThisChain = script.registryAddress(); | |
| 52 | + | |
| 53 | + vm.chainId(137); | |
| 54 | + assertEq(script.registryAddress(), onThisChain); | |
| 55 | + | |
| 56 | + vm.chainId(42_161); | |
| 57 | + assertEq(script.registryAddress(), onThisChain); | |
| 58 | + } | |
| 59 | + | |
| 60 | + /*////////////////////////////////////////////////////////////// | |
| 61 | + THE DERIVATION | |
| 62 | + //////////////////////////////////////////////////////////////*/ | |
| 63 | + | |
| 26 | 64 | function test_DeploysAtPredictedAddress() public { |
| 27 | - address predicted = vm.computeCreate2Address(SALT, keccak256(script.initCode(WETH)), CREATE2_FACTORY); | |
| 65 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); | |
| 28 | 66 | |
| 29 | - Love deployed = Love(_deploy(SALT, WETH)); | |
| 67 | + address predicted = vm.computeCreate2Address(SALT, keccak256(script.initCode()), CREATE2_FACTORY); | |
| 68 | + Love deployed = Love(_deploy(SALT)); | |
| 30 | 69 | |
| 31 | 70 | assertEq(address(deployed), predicted); |
| 32 | 71 | assertEq(deployed.symbol(), "LOVE"); |
| 33 | - assertEq(address(deployed.WETH()), WETH); | |
| 72 | + assertEq(address(deployed.WETH()), OP_STACK_WETH); | |
| 34 | 73 | } |
| 35 | 74 | |
| 36 | 75 | /// @dev What the deploy script prints must be what the chain gives back. |
| 37 | 76 | function test_ScriptPredictionMatchesDeployment() public { |
| 38 | - assertEq(script.predict(SALT, WETH), _deploy(SALT, WETH)); | |
| 77 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); | |
| 78 | + | |
| 79 | + assertEq(script.predict(SALT), _deploy(SALT)); | |
| 39 | 80 | } |
| 40 | 81 | |
| 41 | 82 | function test_ScriptUsesKeccakOfLoveAsDefaultSalt() public view { |
| @@ -43,46 +84,89 @@ contract LoveCreate2Test is Test { | ||
| 43 | 84 | } |
| 44 | 85 | |
| 45 | 86 | function test_ScriptDefaultsToOpStackWeth() public view { |
| 46 | - assertEq(script.DEFAULT_WETH(), WETH); | |
| 87 | + assertEq(script.DEFAULT_WETH(), OP_STACK_WETH); | |
| 47 | 88 | } |
| 48 | 89 | |
| 49 | - /// @dev Same salt, same code, same wETH, same address. | |
| 90 | + /// @dev Same salt, same code, same address — and unlike before, no wETH | |
| 91 | + /// argument that could make the answer differ. | |
| 50 | 92 | function test_PredictionIsChainAgnostic() public { |
| 51 | - address onThisChain = script.predict(SALT, WETH); | |
| 93 | + address onThisChain = script.predict(SALT); | |
| 52 | 94 | |
| 53 | 95 | vm.chainId(137); |
| 54 | - assertEq(script.predict(SALT, WETH), onThisChain); | |
| 96 | + assertEq(script.predict(SALT), onThisChain); | |
| 55 | 97 | |
| 56 | 98 | vm.chainId(42_161); |
| 57 | - assertEq(script.predict(SALT, WETH), onThisChain); | |
| 99 | + assertEq(script.predict(SALT), onThisChain); | |
| 58 | 100 | } |
| 59 | 101 | |
| 60 | 102 | function test_DifferentSaltsGiveDifferentAddresses() public view { |
| 61 | - assertTrue(script.predict(SALT, WETH) != script.predict(keccak256("LOVE2"), WETH)); | |
| 103 | + assertTrue(script.predict(SALT) != script.predict(keccak256("LOVE2"))); | |
| 62 | 104 | } |
| 63 | 105 | |
| 64 | - /// @dev The wETH address is part of the creation code, so chains with their | |
| 65 | - /// own wETH get their own token address. | |
| 66 | - function test_DifferentWethGivesDifferentAddress() public view { | |
| 67 | - assertTrue(script.predict(SALT, WETH) != script.predict(SALT, address(0xBEEF))); | |
| 106 | + /*////////////////////////////////////////////////////////////// | |
| 107 | + THE POINT OF ALL THIS | |
| 108 | + //////////////////////////////////////////////////////////////*/ | |
| 109 | + | |
| 110 | + /// @dev The test that replaces `test_DifferentWethGivesDifferentAddress`. | |
| 111 | + /// Two chains, two different wETH implementations at two different | |
| 112 | + /// addresses, one LOVE address. This is what the registry buys. | |
| 113 | + function test_SameAddressOnChainsWithDifferentWeth() public { | |
| 114 | + // A snapshot stands in for a second chain. Etching the code away is | |
| 115 | + // not enough: a deployed account keeps its nonce, and CREATE2 refuses | |
| 116 | + // to build over that, so the registry could never be redeployed. | |
| 117 | + uint256 freshChain = vm.snapshotState(); | |
| 118 | + | |
| 119 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); | |
| 120 | + address onOpStack = _deploy(SALT); | |
| 121 | + assertEq(address(Love(onOpStack).WETH()), OP_STACK_WETH); | |
| 122 | + | |
| 123 | + vm.revertToState(freshChain); | |
| 124 | + | |
| 125 | + // Different chain id, a different wETH implementation, at a different | |
| 126 | + // address. | |
| 127 | + vm.chainId(1); | |
| 128 | + setUpChain(MAINNET_WETH, WETH9_CANONICAL); | |
| 129 | + address onMainnet = _deploy(SALT); | |
| 130 | + assertEq(address(Love(onMainnet).WETH()), MAINNET_WETH); | |
| 131 | + | |
| 132 | + assertEq(onOpStack, onMainnet, "LOVE must land at one address on both chains"); | |
| 68 | 133 | } |
| 69 | 134 | |
| 70 | 135 | /// @dev Redeploying with the same salt must fail, not silently return the |
| 71 | 136 | /// existing token. |
| 72 | 137 | function test_RevertWhen_RedeployingWithSameSalt() public { |
| 73 | - _deploy(SALT, WETH); | |
| 138 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); | |
| 139 | + _deploy(SALT); | |
| 74 | 140 | |
| 75 | - (bool ok,) = CREATE2_FACTORY.call(abi.encodePacked(SALT, script.initCode(WETH))); | |
| 141 | + (bool ok,) = CREATE2_FACTORY.call(abi.encodePacked(SALT, script.initCode())); | |
| 76 | 142 | assertFalse(ok); |
| 77 | 143 | } |
| 78 | 144 | |
| 79 | - function testFuzz_PredictionMatchesDeployment(bytes32 salt, address weth) public { | |
| 80 | - assertEq(script.predict(salt, weth), _deploy(salt, weth)); | |
| 145 | + function testFuzz_PredictionMatchesDeployment(bytes32 salt) public { | |
| 146 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); | |
| 147 | + | |
| 148 | + assertEq(script.predict(salt), _deploy(salt)); | |
| 149 | + } | |
| 150 | + | |
| 151 | + /*////////////////////////////////////////////////////////////// | |
| 152 | + CONSTRUCTOR GUARDS | |
| 153 | + //////////////////////////////////////////////////////////////*/ | |
| 154 | + | |
| 155 | + function test_RevertWhen_RegistryNotDeployed() public { | |
| 156 | + vm.expectRevert(abi.encodeWithSelector(Love.RegistryNotDeployed.selector, script.registryAddress())); | |
| 157 | + new Love(); | |
| 158 | + } | |
| 159 | + | |
| 160 | + function test_RevertWhen_WethNotRegistered() public { | |
| 161 | + deployRegistry(); | |
| 162 | + | |
| 163 | + vm.expectRevert(abi.encodeWithSelector(Love.WethNotRegistered.selector, script.registryAddress())); | |
| 164 | + new Love(); | |
| 81 | 165 | } |
| 82 | 166 | |
| 83 | 167 | /// @dev Deploys through the canonical deterministic deployer, as the script does. |
| 84 | - function _deploy(bytes32 salt, address weth) internal returns (address deployed) { | |
| 85 | - (bool ok, bytes memory ret) = CREATE2_FACTORY.call(abi.encodePacked(salt, script.initCode(weth))); | |
| 168 | + function _deploy(bytes32 salt) internal returns (address deployed) { | |
| 169 | + (bool ok, bytes memory ret) = CREATE2_FACTORY.call(abi.encodePacked(salt, script.initCode())); | |
| 86 | 170 | require(ok, "create2 deployment failed"); |
| 87 | 171 | // casting to 'bytes20' is safe because the deployer returns the 20-byte address, raw |
| 88 | 172 | // forge-lint: disable-next-line(unsafe-typecast) |
| @@ -3,16 +3,20 @@ pragma solidity ^0.8.30; | |||
| 3 | 3 | ||
| 4 | import {LoveScript} from "../script/Love.s.sol"; | 4 | import {LoveScript} from "../script/Love.s.sol"; |
| 5 | import {Love} from "../src/Love.sol"; | 5 | import {Love} from "../src/Love.sol"; |
| 6 | +import {Fixtures} from "./Fixtures.sol"; | ||
| 6 | import {Test} from "forge-std/Test.sol"; | 7 | import {Test} from "forge-std/Test.sol"; |
| 7 | 8 | ||
| 8 | /// @title LoveCreate2Test | 9 | /// @title LoveCreate2Test |
| 9 | -/// @notice The deployment address derives from the salt, the creation code and | 10 | +/// @notice The deployment address now derives from the salt and the creation |
| 10 | -/// the wETH constructor argument. These tests pin that derivation: same | 11 | +/// code alone. wETH used to be a constructor argument and so part of |
| 11 | -/// salt and same wETH give the same address on any chain, a different | 12 | +/// that creation code, which gave the token a different address on |
| 12 | -/// wETH gives a different one. | 13 | +/// every chain whose wETH sat elsewhere; it is read from the registry |
| 13 | -contract LoveCreate2Test is Test { | 14 | +/// instead. These tests pin the consequence: same salt, same address, |
| 15 | +/// on any chain, whatever its wETH is and wherever it lives. | ||
| 16 | +contract LoveCreate2Test is Test, Fixtures { | ||
| 14 | bytes32 constant SALT = keccak256("LOVE"); | 17 | bytes32 constant SALT = keccak256("LOVE"); |
| 15 | - address constant WETH = 0x4200000000000000000000000000000000000006; | 18 | + address constant OP_STACK_WETH = 0x4200000000000000000000000000000000000006; |
| 19 | + address constant MAINNET_WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; | ||
| 16 | 20 | ||
| 17 | LoveScript script; | 21 | LoveScript script; |
| 18 | 22 | ||
| @@ -23,19 +27,56 @@ contract LoveCreate2Test is Test { | |||
| 23 | assertGt(CREATE2_FACTORY.code.length, 0, "create2 deployer missing"); | 27 | assertGt(CREATE2_FACTORY.code.length, 0, "create2 deployer missing"); |
| 24 | } | 28 | } |
| 25 | 29 | ||
| 30 | + /*////////////////////////////////////////////////////////////// | ||
| 31 | + THE REGISTRY | ||
| 32 | + //////////////////////////////////////////////////////////////*/ | ||
| 33 | + | ||
| 34 | + /// @dev `Love` has the registry's address compiled in, so if the registry's | ||
| 35 | + /// creation code moves — an edit to the contract, a solc bump, a | ||
| 36 | + /// change of optimizer settings — this fails rather than shipping a | ||
| 37 | + /// token pointing at an empty address. | ||
| 38 | + function test_LoveRegistryConstantMatchesDeterministicAddress() public { | ||
| 39 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); | ||
| 40 | + | ||
| 41 | + assertEq(new Love().REGISTRY(), script.registryAddress()); | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + function test_RegistryLandsAtTheAddressLoveExpects() public { | ||
| 45 | + assertEq(address(deployRegistry()), script.registryAddress()); | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + /// @dev The registry takes no constructor arguments, which is what makes | ||
| 49 | + /// its address the same everywhere. | ||
| 50 | + function test_RegistryAddressIsChainAgnostic() public { | ||
| 51 | + address onThisChain = script.registryAddress(); | ||
| 52 | + | ||
| 53 | + vm.chainId(137); | ||
| 54 | + assertEq(script.registryAddress(), onThisChain); | ||
| 55 | + | ||
| 56 | + vm.chainId(42_161); | ||
| 57 | + assertEq(script.registryAddress(), onThisChain); | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + /*////////////////////////////////////////////////////////////// | ||
| 61 | + THE DERIVATION | ||
| 62 | + //////////////////////////////////////////////////////////////*/ | ||
| 63 | + | ||
| 26 | function test_DeploysAtPredictedAddress() public { | 64 | function test_DeploysAtPredictedAddress() public { |
| 27 | - address predicted = vm.computeCreate2Address(SALT, keccak256(script.initCode(WETH)), CREATE2_FACTORY); | 65 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); |
| 28 | 66 | ||
| 29 | - Love deployed = Love(_deploy(SALT, WETH)); | 67 | + address predicted = vm.computeCreate2Address(SALT, keccak256(script.initCode()), CREATE2_FACTORY); |
| 68 | + Love deployed = Love(_deploy(SALT)); | ||
| 30 | 69 | ||
| 31 | assertEq(address(deployed), predicted); | 70 | assertEq(address(deployed), predicted); |
| 32 | assertEq(deployed.symbol(), "LOVE"); | 71 | assertEq(deployed.symbol(), "LOVE"); |
| 33 | - assertEq(address(deployed.WETH()), WETH); | 72 | + assertEq(address(deployed.WETH()), OP_STACK_WETH); |
| 34 | } | 73 | } |
| 35 | 74 | ||
| 36 | /// @dev What the deploy script prints must be what the chain gives back. | 75 | /// @dev What the deploy script prints must be what the chain gives back. |
| 37 | function test_ScriptPredictionMatchesDeployment() public { | 76 | function test_ScriptPredictionMatchesDeployment() public { |
| 38 | - assertEq(script.predict(SALT, WETH), _deploy(SALT, WETH)); | 77 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); |
| 78 | + | ||
| 79 | + assertEq(script.predict(SALT), _deploy(SALT)); | ||
| 39 | } | 80 | } |
| 40 | 81 | ||
| 41 | function test_ScriptUsesKeccakOfLoveAsDefaultSalt() public view { | 82 | function test_ScriptUsesKeccakOfLoveAsDefaultSalt() public view { |
| @@ -43,46 +84,89 @@ contract LoveCreate2Test is Test { | |||
| 43 | } | 84 | } |
| 44 | 85 | ||
| 45 | function test_ScriptDefaultsToOpStackWeth() public view { | 86 | function test_ScriptDefaultsToOpStackWeth() public view { |
| 46 | - assertEq(script.DEFAULT_WETH(), WETH); | 87 | + assertEq(script.DEFAULT_WETH(), OP_STACK_WETH); |
| 47 | } | 88 | } |
| 48 | 89 | ||
| 49 | - /// @dev Same salt, same code, same wETH, same address. | 90 | + /// @dev Same salt, same code, same address — and unlike before, no wETH |
| 91 | + /// argument that could make the answer differ. | ||
| 50 | function test_PredictionIsChainAgnostic() public { | 92 | function test_PredictionIsChainAgnostic() public { |
| 51 | - address onThisChain = script.predict(SALT, WETH); | 93 | + address onThisChain = script.predict(SALT); |
| 52 | 94 | ||
| 53 | vm.chainId(137); | 95 | vm.chainId(137); |
| 54 | - assertEq(script.predict(SALT, WETH), onThisChain); | 96 | + assertEq(script.predict(SALT), onThisChain); |
| 55 | 97 | ||
| 56 | vm.chainId(42_161); | 98 | vm.chainId(42_161); |
| 57 | - assertEq(script.predict(SALT, WETH), onThisChain); | 99 | + assertEq(script.predict(SALT), onThisChain); |
| 58 | } | 100 | } |
| 59 | 101 | ||
| 60 | function test_DifferentSaltsGiveDifferentAddresses() public view { | 102 | function test_DifferentSaltsGiveDifferentAddresses() public view { |
| 61 | - assertTrue(script.predict(SALT, WETH) != script.predict(keccak256("LOVE2"), WETH)); | 103 | + assertTrue(script.predict(SALT) != script.predict(keccak256("LOVE2"))); |
| 62 | } | 104 | } |
| 63 | 105 | ||
| 64 | - /// @dev The wETH address is part of the creation code, so chains with their | 106 | + /*////////////////////////////////////////////////////////////// |
| 65 | - /// own wETH get their own token address. | 107 | + THE POINT OF ALL THIS |
| 66 | - function test_DifferentWethGivesDifferentAddress() public view { | 108 | + //////////////////////////////////////////////////////////////*/ |
| 67 | - assertTrue(script.predict(SALT, WETH) != script.predict(SALT, address(0xBEEF))); | 109 | + |
| 110 | + /// @dev The test that replaces `test_DifferentWethGivesDifferentAddress`. | ||
| 111 | + /// Two chains, two different wETH implementations at two different | ||
| 112 | + /// addresses, one LOVE address. This is what the registry buys. | ||
| 113 | + function test_SameAddressOnChainsWithDifferentWeth() public { | ||
| 114 | + // A snapshot stands in for a second chain. Etching the code away is | ||
| 115 | + // not enough: a deployed account keeps its nonce, and CREATE2 refuses | ||
| 116 | + // to build over that, so the registry could never be redeployed. | ||
| 117 | + uint256 freshChain = vm.snapshotState(); | ||
| 118 | + | ||
| 119 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); | ||
| 120 | + address onOpStack = _deploy(SALT); | ||
| 121 | + assertEq(address(Love(onOpStack).WETH()), OP_STACK_WETH); | ||
| 122 | + | ||
| 123 | + vm.revertToState(freshChain); | ||
| 124 | + | ||
| 125 | + // Different chain id, a different wETH implementation, at a different | ||
| 126 | + // address. | ||
| 127 | + vm.chainId(1); | ||
| 128 | + setUpChain(MAINNET_WETH, WETH9_CANONICAL); | ||
| 129 | + address onMainnet = _deploy(SALT); | ||
| 130 | + assertEq(address(Love(onMainnet).WETH()), MAINNET_WETH); | ||
| 131 | + | ||
| 132 | + assertEq(onOpStack, onMainnet, "LOVE must land at one address on both chains"); | ||
| 68 | } | 133 | } |
| 69 | 134 | ||
| 70 | /// @dev Redeploying with the same salt must fail, not silently return the | 135 | /// @dev Redeploying with the same salt must fail, not silently return the |
| 71 | /// existing token. | 136 | /// existing token. |
| 72 | function test_RevertWhen_RedeployingWithSameSalt() public { | 137 | function test_RevertWhen_RedeployingWithSameSalt() public { |
| 73 | - _deploy(SALT, WETH); | 138 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); |
| 139 | + _deploy(SALT); | ||
| 74 | 140 | ||
| 75 | - (bool ok,) = CREATE2_FACTORY.call(abi.encodePacked(SALT, script.initCode(WETH))); | 141 | + (bool ok,) = CREATE2_FACTORY.call(abi.encodePacked(SALT, script.initCode())); |
| 76 | assertFalse(ok); | 142 | assertFalse(ok); |
| 77 | } | 143 | } |
| 78 | 144 | ||
| 79 | - function testFuzz_PredictionMatchesDeployment(bytes32 salt, address weth) public { | 145 | + function testFuzz_PredictionMatchesDeployment(bytes32 salt) public { |
| 80 | - assertEq(script.predict(salt, weth), _deploy(salt, weth)); | 146 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); |
| 147 | + | ||
| 148 | + assertEq(script.predict(salt), _deploy(salt)); | ||
| 149 | + } | ||
| 150 | + | ||
| 151 | + /*////////////////////////////////////////////////////////////// | ||
| 152 | + CONSTRUCTOR GUARDS | ||
| 153 | + //////////////////////////////////////////////////////////////*/ | ||
| 154 | + | ||
| 155 | + function test_RevertWhen_RegistryNotDeployed() public { | ||
| 156 | + vm.expectRevert(abi.encodeWithSelector(Love.RegistryNotDeployed.selector, script.registryAddress())); | ||
| 157 | + new Love(); | ||
| 158 | + } | ||
| 159 | + | ||
| 160 | + function test_RevertWhen_WethNotRegistered() public { | ||
| 161 | + deployRegistry(); | ||
| 162 | + | ||
| 163 | + vm.expectRevert(abi.encodeWithSelector(Love.WethNotRegistered.selector, script.registryAddress())); | ||
| 164 | + new Love(); | ||
| 81 | } | 165 | } |
| 82 | 166 | ||
| 83 | /// @dev Deploys through the canonical deterministic deployer, as the script does. | 167 | /// @dev Deploys through the canonical deterministic deployer, as the script does. |
| 84 | - function _deploy(bytes32 salt, address weth) internal returns (address deployed) { | 168 | + function _deploy(bytes32 salt) internal returns (address deployed) { |
| 85 | - (bool ok, bytes memory ret) = CREATE2_FACTORY.call(abi.encodePacked(salt, script.initCode(weth))); | 169 | + (bool ok, bytes memory ret) = CREATE2_FACTORY.call(abi.encodePacked(salt, script.initCode())); |
| 86 | require(ok, "create2 deployment failed"); | 170 | require(ok, "create2 deployment failed"); |
| 87 | // casting to 'bytes20' is safe because the deployer returns the 20-byte address, raw | 171 | // casting to 'bytes20' is safe because the deployer returns the 20-byte address, raw |
| 88 | // forge-lint: disable-next-line(unsafe-typecast) | 172 | // forge-lint: disable-next-line(unsafe-typecast) |
added
test/fixtures/README.md +22 -0 | new file mode 100644 | ||
| @@ -0,0 +1,22 @@ | ||
| 1 | +# Fixtures | |
| 2 | + | |
| 3 | +Runtime bytecode of two wETH implementations on the allowlist, pulled straight | |
| 4 | +off mainnets so the tests exercise real code rather than a mock that resembles | |
| 5 | +it. `vm.etch` puts one of these at an address and its `EXTCODEHASH` is then, by | |
| 6 | +construction, the value `WETHRegistry` is looking for — which is the only way | |
| 7 | +to test the allowlist without weakening it for tests. | |
| 8 | + | |
| 9 | +| file | source | bytes | codehash | | |
| 10 | +| --- | --- | --- | --- | | |
| 11 | +| `weth9-op-legacy.hex` | Base, `0x4200000000000000000000000000000000000006` | 2041 | `0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c` | | |
| 12 | +| `weth9-canonical.hex` | Ethereum, `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` | 3124 | `0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23` | | |
| 13 | + | |
| 14 | +Refetch either with: | |
| 15 | + | |
| 16 | +```shell | |
| 17 | +cast code 0x4200000000000000000000000000000000000006 --rpc-url https://base-rpc.publicnode.com | |
| 18 | +``` | |
| 19 | + | |
| 20 | +Both hashes appear in `script/weth-codehashes.sh` output and in the allowlist in | |
| 21 | +`src/WETHRegistry.sol`, so a fixture that has drifted shows up as a test | |
| 22 | +failure rather than as a silent pass. | |
| new file mode 100644 | |||
| @@ -0,0 +1,22 @@ | |||
| 1 | +# Fixtures | ||
| 2 | + | ||
| 3 | +Runtime bytecode of two wETH implementations on the allowlist, pulled straight | ||
| 4 | +off mainnets so the tests exercise real code rather than a mock that resembles | ||
| 5 | +it. `vm.etch` puts one of these at an address and its `EXTCODEHASH` is then, by | ||
| 6 | +construction, the value `WETHRegistry` is looking for — which is the only way | ||
| 7 | +to test the allowlist without weakening it for tests. | ||
| 8 | + | ||
| 9 | +| file | source | bytes | codehash | | ||
| 10 | +| --- | --- | --- | --- | | ||
| 11 | +| `weth9-op-legacy.hex` | Base, `0x4200000000000000000000000000000000000006` | 2041 | `0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c` | | ||
| 12 | +| `weth9-canonical.hex` | Ethereum, `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` | 3124 | `0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23` | | ||
| 13 | + | ||
| 14 | +Refetch either with: | ||
| 15 | + | ||
| 16 | +```shell | ||
| 17 | +cast code 0x4200000000000000000000000000000000000006 --rpc-url https://base-rpc.publicnode.com | ||
| 18 | +``` | ||
| 19 | + | ||
| 20 | +Both hashes appear in `script/weth-codehashes.sh` output and in the allowlist in | ||
| 21 | +`src/WETHRegistry.sol`, so a fixture that has drifted shows up as a test | ||
| 22 | +failure rather than as a silent pass. | ||
added
test/fixtures/weth9-canonical.hex +1 -0 | new file mode 100644 | ||
| @@ -0,0 +1 @@ | ||
| 1 | +0x6060604052600436106100af576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306fdde03146100b9578063095ea7b31461014757806318160ddd146101a157806323b872dd146101ca5780632e1a7d4d14610243578063313ce5671461026657806370a082311461029557806395d89b41146102e2578063a9059cbb14610370578063d0e30db0146103ca578063dd62ed3e146103d4575b6100b7610440565b005b34156100c457600080fd5b6100cc6104dd565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561010c5780820151818401526020810190506100f1565b50505050905090810190601f1680156101395780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561015257600080fd5b610187600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061057b565b604051808215151515815260200191505060405180910390f35b34156101ac57600080fd5b6101b461066d565b6040518082815260200191505060405180910390f35b34156101d557600080fd5b610229600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061068c565b604051808215151515815260200191505060405180910390f35b341561024e57600080fd5b61026460048080359060200190919050506109d9565b005b341561027157600080fd5b610279610b05565b604051808260ff1660ff16815260200191505060405180910390f35b34156102a057600080fd5b6102cc600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610b18565b6040518082815260200191505060405180910390f35b34156102ed57600080fd5b6102f5610b30565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561033557808201518184015260208101905061031a565b50505050905090810190601f1680156103625780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561037b57600080fd5b6103b0600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091908035906020019091905050610bce565b604051808215151515815260200191505060405180910390f35b6103d2610440565b005b34156103df57600080fd5b61042a600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610be3565b6040518082815260200191505060405180910390f35b34600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055503373ffffffffffffffffffffffffffffffffffffffff167fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c346040518082815260200191505060405180910390a2565b60008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156105735780601f1061054857610100808354040283529160200191610573565b820191906000526020600020905b81548152906001019060200180831161055657829003601f168201915b505050505081565b600081600460003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040518082815260200191505060405180910390a36001905092915050565b60003073ffffffffffffffffffffffffffffffffffffffff1631905090565b600081600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054101515156106dc57600080fd5b3373ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff16141580156107b457507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205414155b156108cf5781600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020541015151561084457600080fd5b81600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055505b81600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254039250508190555081600360008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a3600190509392505050565b80600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410151515610a2757600080fd5b80600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f193505050501515610ab457600080fd5b3373ffffffffffffffffffffffffffffffffffffffff167f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65826040518082815260200191505060405180910390a250565b600260009054906101000a900460ff1681565b60036020528060005260406000206000915090505481565b60018054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610bc65780601f10610b9b57610100808354040283529160200191610bc6565b820191906000526020600020905b815481529060010190602001808311610ba957829003601f168201915b505050505081565b6000610bdb33848461068c565b905092915050565b60046020528160005260406000206020528060005260406000206000915091505054815600a165627a7a72305820deb4c2ccab3c2fdca32ab3f46728389c2fe2c165d5fafa07661e4e004f6c344a0029 | |
| new file mode 100644 | |||
| @@ -0,0 +1 @@ | |||
| 1 | +0x6060604052600436106100af576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306fdde03146100b9578063095ea7b31461014757806318160ddd146101a157806323b872dd146101ca5780632e1a7d4d14610243578063313ce5671461026657806370a082311461029557806395d89b41146102e2578063a9059cbb14610370578063d0e30db0146103ca578063dd62ed3e146103d4575b6100b7610440565b005b34156100c457600080fd5b6100cc6104dd565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561010c5780820151818401526020810190506100f1565b50505050905090810190601f1680156101395780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561015257600080fd5b610187600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061057b565b604051808215151515815260200191505060405180910390f35b34156101ac57600080fd5b6101b461066d565b6040518082815260200191505060405180910390f35b34156101d557600080fd5b610229600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061068c565b604051808215151515815260200191505060405180910390f35b341561024e57600080fd5b61026460048080359060200190919050506109d9565b005b341561027157600080fd5b610279610b05565b604051808260ff1660ff16815260200191505060405180910390f35b34156102a057600080fd5b6102cc600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610b18565b6040518082815260200191505060405180910390f35b34156102ed57600080fd5b6102f5610b30565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561033557808201518184015260208101905061031a565b50505050905090810190601f1680156103625780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561037b57600080fd5b6103b0600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091908035906020019091905050610bce565b604051808215151515815260200191505060405180910390f35b6103d2610440565b005b34156103df57600080fd5b61042a600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610be3565b6040518082815260200191505060405180910390f35b34600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055503373ffffffffffffffffffffffffffffffffffffffff167fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c346040518082815260200191505060405180910390a2565b60008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156105735780601f1061054857610100808354040283529160200191610573565b820191906000526020600020905b81548152906001019060200180831161055657829003601f168201915b505050505081565b600081600460003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040518082815260200191505060405180910390a36001905092915050565b60003073ffffffffffffffffffffffffffffffffffffffff1631905090565b600081600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054101515156106dc57600080fd5b3373ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff16141580156107b457507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205414155b156108cf5781600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020541015151561084457600080fd5b81600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055505b81600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254039250508190555081600360008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a3600190509392505050565b80600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410151515610a2757600080fd5b80600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f193505050501515610ab457600080fd5b3373ffffffffffffffffffffffffffffffffffffffff167f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65826040518082815260200191505060405180910390a250565b600260009054906101000a900460ff1681565b60036020528060005260406000206000915090505481565b60018054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610bc65780601f10610b9b57610100808354040283529160200191610bc6565b820191906000526020600020905b815481529060010190602001808311610ba957829003601f168201915b505050505081565b6000610bdb33848461068c565b905092915050565b60046020528160005260406000206020528060005260406000206000915091505054815600a165627a7a72305820deb4c2ccab3c2fdca32ab3f46728389c2fe2c165d5fafa07661e4e004f6c344a0029 | ||
added
test/fixtures/weth9-op-legacy.hex +1 -0 | new file mode 100644 | ||
| @@ -0,0 +1 @@ | ||
| 1 | +0x6080604052600436106100bc5760003560e01c8063313ce56711610074578063a9059cbb1161004e578063a9059cbb146102cb578063d0e30db0146100bc578063dd62ed3e14610311576100bc565b8063313ce5671461024b57806370a082311461027657806395d89b41146102b6576100bc565b806318160ddd116100a557806318160ddd146101aa57806323b872dd146101d15780632e1a7d4d14610221576100bc565b806306fdde03146100c6578063095ea7b314610150575b6100c4610359565b005b3480156100d257600080fd5b506100db6103a8565b6040805160208082528351818301528351919283929083019185019080838360005b838110156101155781810151838201526020016100fd565b50505050905090810190601f1680156101425780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561015c57600080fd5b506101966004803603604081101561017357600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135610454565b604080519115158252519081900360200190f35b3480156101b657600080fd5b506101bf6104c7565b60408051918252519081900360200190f35b3480156101dd57600080fd5b50610196600480360360608110156101f457600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135811691602081013590911690604001356104cb565b34801561022d57600080fd5b506100c46004803603602081101561024457600080fd5b503561066b565b34801561025757600080fd5b50610260610700565b6040805160ff9092168252519081900360200190f35b34801561028257600080fd5b506101bf6004803603602081101561029957600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16610709565b3480156102c257600080fd5b506100db61071b565b3480156102d757600080fd5b50610196600480360360408110156102ee57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135610793565b34801561031d57600080fd5b506101bf6004803603604081101561033457600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160200135166107a7565b33600081815260036020908152604091829020805434908101909155825190815291517fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c9281900390910190a2565b6000805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f8101849004840282018401909252818152929183018282801561044c5780601f106104215761010080835404028352916020019161044c565b820191906000526020600020905b81548152906001019060200180831161042f57829003601f168201915b505050505081565b33600081815260046020908152604080832073ffffffffffffffffffffffffffffffffffffffff8716808552908352818420869055815186815291519394909390927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925928290030190a350600192915050565b4790565b73ffffffffffffffffffffffffffffffffffffffff83166000908152600360205260408120548211156104fd57600080fd5b73ffffffffffffffffffffffffffffffffffffffff84163314801590610573575073ffffffffffffffffffffffffffffffffffffffff841660009081526004602090815260408083203384529091529020547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff14155b156105ed5773ffffffffffffffffffffffffffffffffffffffff841660009081526004602090815260408083203384529091529020548211156105b557600080fd5b73ffffffffffffffffffffffffffffffffffffffff841660009081526004602090815260408083203384529091529020805483900390555b73ffffffffffffffffffffffffffffffffffffffff808516600081815260036020908152604080832080548890039055938716808352918490208054870190558351868152935191937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef929081900390910190a35060019392505050565b3360009081526003602052604090205481111561068757600080fd5b33600081815260036020526040808220805485900390555183156108fc0291849190818181858888f193505050501580156106c6573d6000803e3d6000fd5b5060408051828152905133917f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65919081900360200190a250565b60025460ff1681565b60036020526000908152604090205481565b60018054604080516020600284861615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f8101849004840282018401909252818152929183018282801561044c5780601f106104215761010080835404028352916020019161044c565b60006107a03384846104cb565b9392505050565b60046020908152600092835260408084209091529082529020548156fea265627a7a72315820e496abb80c5983b030f680d0bd88f66bf44e261bc3be070d612dd72f9f1f5e9a64736f6c63430005110032 | |
| new file mode 100644 | |||
| @@ -0,0 +1 @@ | |||
| 1 | +0x6080604052600436106100bc5760003560e01c8063313ce56711610074578063a9059cbb1161004e578063a9059cbb146102cb578063d0e30db0146100bc578063dd62ed3e14610311576100bc565b8063313ce5671461024b57806370a082311461027657806395d89b41146102b6576100bc565b806318160ddd116100a557806318160ddd146101aa57806323b872dd146101d15780632e1a7d4d14610221576100bc565b806306fdde03146100c6578063095ea7b314610150575b6100c4610359565b005b3480156100d257600080fd5b506100db6103a8565b6040805160208082528351818301528351919283929083019185019080838360005b838110156101155781810151838201526020016100fd565b50505050905090810190601f1680156101425780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561015c57600080fd5b506101966004803603604081101561017357600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135610454565b604080519115158252519081900360200190f35b3480156101b657600080fd5b506101bf6104c7565b60408051918252519081900360200190f35b3480156101dd57600080fd5b50610196600480360360608110156101f457600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135811691602081013590911690604001356104cb565b34801561022d57600080fd5b506100c46004803603602081101561024457600080fd5b503561066b565b34801561025757600080fd5b50610260610700565b6040805160ff9092168252519081900360200190f35b34801561028257600080fd5b506101bf6004803603602081101561029957600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16610709565b3480156102c257600080fd5b506100db61071b565b3480156102d757600080fd5b50610196600480360360408110156102ee57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135610793565b34801561031d57600080fd5b506101bf6004803603604081101561033457600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160200135166107a7565b33600081815260036020908152604091829020805434908101909155825190815291517fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c9281900390910190a2565b6000805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f8101849004840282018401909252818152929183018282801561044c5780601f106104215761010080835404028352916020019161044c565b820191906000526020600020905b81548152906001019060200180831161042f57829003601f168201915b505050505081565b33600081815260046020908152604080832073ffffffffffffffffffffffffffffffffffffffff8716808552908352818420869055815186815291519394909390927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925928290030190a350600192915050565b4790565b73ffffffffffffffffffffffffffffffffffffffff83166000908152600360205260408120548211156104fd57600080fd5b73ffffffffffffffffffffffffffffffffffffffff84163314801590610573575073ffffffffffffffffffffffffffffffffffffffff841660009081526004602090815260408083203384529091529020547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff14155b156105ed5773ffffffffffffffffffffffffffffffffffffffff841660009081526004602090815260408083203384529091529020548211156105b557600080fd5b73ffffffffffffffffffffffffffffffffffffffff841660009081526004602090815260408083203384529091529020805483900390555b73ffffffffffffffffffffffffffffffffffffffff808516600081815260036020908152604080832080548890039055938716808352918490208054870190558351868152935191937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef929081900390910190a35060019392505050565b3360009081526003602052604090205481111561068757600080fd5b33600081815260036020526040808220805485900390555183156108fc0291849190818181858888f193505050501580156106c6573d6000803e3d6000fd5b5060408051828152905133917f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65919081900360200190a250565b60025460ff1681565b60036020526000908152604090205481565b60018054604080516020600284861615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f8101849004840282018401909252818152929183018282801561044c5780601f106104215761010080835404028352916020019161044c565b60006107a03384846104cb565b9392505050565b60046020908152600092835260408084209091529082529020548156fea265627a7a72315820e496abb80c5983b030f680d0bd88f66bf44e261bc3be070d612dd72f9f1f5e9a64736f6c63430005110032 | ||