julien/lovepublic Fork
⑂ bb4dfe3
Commits
⬇ Clone ▾
git clone https://git.rickub.com/julien/love.git
git clone ssh://git@rickub.com/julien/love.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

Merge 10-fix-weth-chain-allowlist into main

rickub committed 2026-09-18T16:22:15Z Browse files
bb4dfe3 parent: a62812f
modified docs/MULTICHAIN.md +1 -1
@@ -22,7 +22,7 @@ The part that makes this non-trivial: `Love` still needs to know which [wETH](ht
2222
2323 ## The two-contract setup
2424
25-`WETHRegistry` deploys first, with a fixed salt (`keccak256("LOVE.WETHRegistry")`), landing at the same address on every chain: `0x9Cf17430fEdEC487518416D1Cdc849b1eE9CDbA3`, hardcoded into `Love` as `REGISTRY`.
25+`WETHRegistry` deploys first, with a fixed salt (`keccak256("LOVE.WETHRegistry")`), landing at the same address on every chain: `0x33dF1Cd28c9d577224F9A79dD9541275BCa70998`, hardcoded into `Love` as `REGISTRY`.
2626
2727 `register(candidate)` is permissionless, payable exactly `1 wei`, and accepts a candidate only if it passes two gates:
2828
@@ -22,7 +22,7 @@ The part that makes this non-trivial: `Love` still needs to know which [wETH](ht
22 22
23 ## The two-contract setup23 ## The two-contract setup
24 24
25-`WETHRegistry` deploys first, with a fixed salt (`keccak256("LOVE.WETHRegistry")`), landing at the same address on every chain: `0x9Cf17430fEdEC487518416D1Cdc849b1eE9CDbA3`, hardcoded into `Love` as `REGISTRY`.25+`WETHRegistry` deploys first, with a fixed salt (`keccak256("LOVE.WETHRegistry")`), landing at the same address on every chain: `0x33dF1Cd28c9d577224F9A79dD9541275BCa70998`, hardcoded into `Love` as `REGISTRY`.
26 26
27 `register(candidate)` is permissionless, payable exactly `1 wei`, and accepts a candidate only if it passes two gates:27 `register(candidate)` is permissionless, payable exactly `1 wei`, and accepts a candidate only if it passes two gates:
28 28
modified src/Love.sol +1 -1
@@ -35,7 +35,7 @@ contract Love is ERC20 {
3535 /// `WETHRegistry`, the solc version or the optimizer settings change.
3636 /// `LoveRegistryAddressTest` recomputes it and fails if this constant
3737 /// has drifted.
38- address public constant REGISTRY = 0x9Cf17430fEdEC487518416D1Cdc849b1eE9CDbA3;
38+ address public constant REGISTRY = 0x33dF1Cd28c9d577224F9A79dD9541275BCa70998;
3939
4040 /// @notice The wETH this token is pegged to and collateralised with.
4141 /// @dev Read once from the registry and immutable thereafter. The registry
@@ -35,7 +35,7 @@ contract Love is ERC20 {
35 /// `WETHRegistry`, the solc version or the optimizer settings change.35 /// `WETHRegistry`, the solc version or the optimizer settings change.
36 /// `LoveRegistryAddressTest` recomputes it and fails if this constant36 /// `LoveRegistryAddressTest` recomputes it and fails if this constant
37 /// has drifted.37 /// has drifted.
38- address public constant REGISTRY = 0x9Cf17430fEdEC487518416D1Cdc849b1eE9CDbA3;38+ address public constant REGISTRY = 0x33dF1Cd28c9d577224F9A79dD9541275BCa70998;
39 39
40 /// @notice The wETH this token is pegged to and collateralised with.40 /// @notice The wETH this token is pegged to and collateralised with.
41 /// @dev Read once from the registry and immutable thereafter. The registry41 /// @dev Read once from the registry and immutable thereafter. The registry
modified src/WETHRegistry.sol +78 -27
@@ -17,15 +17,23 @@ import {IWETH} from "./IWETH.sol";
1717 ///
1818 /// Two gates run at registration:
1919 ///
20-/// 1. `EXTCODEHASH` against the allowlist below. This is the real
21-/// defence: matching means the candidate is provably one of a handful
22-/// of implementations whose source has been read, not merely something
23-/// that behaves well today. It is what catches a hidden mint, an
24-/// upgrade hook or a backdoor, none of which a behavioural test can
25-/// see.
20+/// 1. `EXTCODEHASH` against the allowlist below, paired with the current
21+/// `block.chainid`. This is the real defence: matching means the
22+/// candidate is provably one of a handful of implementations whose
23+/// source has been read *and reviewed for this chain* — not merely
24+/// something that behaves well today, and not merely bytecode that
25+/// happens to be byte-identical to something reviewed elsewhere.
26+/// Pairing with the chain ID is what stops a chain-agnostic
27+/// implementation like canonical WETH9 — no constructor argument, no
28+/// chain-specific logic, so its bytecode is identical everywhere —
29+/// from being accepted on a chain whose native currency isn't ETH at
30+/// all, where it would still wrap correctly, just the wrong asset.
2631 /// 2. A one-wei deposit/withdraw round trip, which catches the plain
2732 /// mistake of an address that is not wETH at all, and proves the
28-/// wrapper actually works on this chain.
33+/// wrapper actually works on this chain. It cannot, by itself, prove
34+/// the native currency being wrapped is ETH: that fact isn't
35+/// observable from inside the EVM, which is exactly why gate 1 has to
36+/// carry the chain ID.
2937 ///
3038 /// The registration is write-once. There is no setter, no owner and no
3139 /// way to revoke an entry, so a `Love` deployed against this registry can
@@ -61,10 +69,11 @@ contract WETHRegistry {
6169 /// @param registered The wETH registered by the earlier call.
6270 error AlreadyRegistered(IWETH registered);
6371
64- /// @notice Thrown when the candidate's code is not a reviewed wETH.
72+ /// @notice Thrown when the candidate's code is not a reviewed wETH on this chain.
6573 /// @param candidate The rejected address.
6674 /// @param codeHash Its `EXTCODEHASH`, zero if there is no code there.
67- error UnknownImplementation(IWETH candidate, bytes32 codeHash);
75+ /// @param chainId The chain the candidate was rejected on.
76+ error UnknownImplementation(IWETH candidate, bytes32 codeHash, uint256 chainId);
6877
6978 /// @notice Thrown when the call does not carry exactly `PROBE` wei.
7079 /// @param sent The value that came with the call.
@@ -115,7 +124,9 @@ contract WETHRegistry {
115124 if (msg.value != PROBE) revert ProbeValueRequired(msg.value);
116125
117126 bytes32 codeHash = address(candidate).codehash;
118- if (!isKnownImplementation(codeHash)) revert UnknownImplementation(candidate, codeHash);
127+ if (!isKnownImplementation(codeHash, block.chainid)) {
128+ revert UnknownImplementation(candidate, codeHash, block.chainid);
129+ }
119130
120131 _probe(candidate);
121132
@@ -125,7 +136,7 @@ contract WETHRegistry {
125136 return candidate;
126137 }
127138
128- /// @notice Whether `codeHash` is one of the reviewed wETH implementations.
139+ /// @notice Whether `codeHash` is a reviewed wETH implementation on `chainId`.
129140 /// @dev Compile-time, so the list is ownerless and append-only by
130141 /// construction: extending it means publishing a new registry, which
131142 /// leaves every existing deployment exactly as it was. No key can
@@ -140,32 +151,72 @@ contract WETHRegistry {
140151 /// holding for every family added later. Ten constants is the cheaper
141152 /// side of that trade.
142153 ///
143- /// Grouped by implementation. Every value is reproducible from chain
144- /// state with `script/weth-codehashes.sh`.
154+ /// Paired with `chainId` rather than accepted on any chain that
155+ /// reproduces the hash: some of these implementations (canonical
156+ /// WETH9 above all — no constructor argument, no reference to
157+ /// anything chain-specific) compile to the exact same bytecode
158+ /// wherever they're deployed. Accepting the hash alone would mean a
159+ /// chain whose native currency isn't ETH could register a candidate
160+ /// that wraps its native currency flawlessly — the round-trip probe
161+ /// would pass — while wrapping the wrong asset entirely, since
162+ /// nothing observable from inside the EVM distinguishes "native
163+ /// currency is ETH" from "native currency behaves like ETH." Pairing
164+ /// the hash with the specific chain IDs it was actually reviewed on
165+ /// closes that gap: a hash match now means "reviewed on this chain,"
166+ /// not "reviewed somewhere."
167+ ///
168+ /// Grouped by implementation. Every codehash is reproducible from
169+ /// chain state with `script/weth-codehashes.sh`; the paired chain IDs
170+ /// are exactly the chains that script read that hash from.
145171 /// @param codeHash The `EXTCODEHASH` to check.
146- /// @return True if a candidate with this code may be registered.
147- function isKnownImplementation(bytes32 codeHash) public pure returns (bool) {
172+ /// @param chainId The chain the candidate was found on, i.e. `block.chainid`.
173+ /// @return True if a candidate with this code may be registered on this chain.
174+ function isKnownImplementation(bytes32 codeHash, uint256 chainId) public pure returns (bool) {
148175 // OP Stack legacy WETH9, 2041 bytes, solc 0.5.17. One implementation,
149176 // four hashes: these chains run byte-identical code and differ only
150177 // inside solc's metadata blob, which never executes.
151- if (codeHash == 0x779bbf2a738ef09d961c945116197e2ac764c1b39304b2b4418cd4e42668b173) return true; // optimism
152- if (codeHash == 0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c) return true; // base, mode, zora
153- if (codeHash == 0x557c8e14d33f7cd67cad0141e1a49ebf3488a447fc3df7aa66b127778a0383d1) return true; // world-chain
154- if (codeHash == 0xf35fe602ba2a3b96f2e27ff7c8b8010800a8d0d616a5fb1f902e087b590355f3) return true; // lisk
155-
156- // Canonical WETH9, 3124 bytes, solc 0.4.19.
157- if (codeHash == 0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23) return true; // ethereum
158- if (codeHash == 0xa670ec6c272ddec6d328d6f3d5cad65a841a6ab45e8e5cf825150eb458be4f1f) return true; // linea
159- if (codeHash == 0x032e9cab14331328530468e54f1b91777b4d5c9dbbb400884badb32bc4113585) return true; // polygon-zkevm
178+ if (codeHash == 0x779bbf2a738ef09d961c945116197e2ac764c1b39304b2b4418cd4e42668b173) {
179+ return chainId == 10; // optimism
180+ }
181+ if (codeHash == 0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c) {
182+ return chainId == 8453 || chainId == 34_443 || chainId == 7_777_777; // base, mode, zora
183+ }
184+ if (codeHash == 0x557c8e14d33f7cd67cad0141e1a49ebf3488a447fc3df7aa66b127778a0383d1) {
185+ return chainId == 480; // world-chain
186+ }
187+ if (codeHash == 0xf35fe602ba2a3b96f2e27ff7c8b8010800a8d0d616a5fb1f902e087b590355f3) {
188+ return chainId == 1135; // lisk
189+ }
190+
191+ // Canonical WETH9, 3124 bytes, solc 0.4.19. Chain-agnostic bytecode —
192+ // no constructor argument, nothing chain-specific — so it reproduces
193+ // identically anywhere; the chain ID is the only thing standing
194+ // between "reviewed" and "byte-identical to something reviewed
195+ // elsewhere."
196+ if (codeHash == 0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23) {
197+ return chainId == 1; // ethereum
198+ }
199+ if (codeHash == 0xa670ec6c272ddec6d328d6f3d5cad65a841a6ab45e8e5cf825150eb458be4f1f) {
200+ return chainId == 59_144; // linea
201+ }
202+ if (codeHash == 0x032e9cab14331328530468e54f1b91777b4d5c9dbbb400884badb32bc4113585) {
203+ return chainId == 1101; // polygon-zkevm
204+ }
160205
161206 // OP Stack WETH, 2865 bytes, solc 0.8.15.
162- if (codeHash == 0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012) return true; // unichain, soneium, ink
207+ if (codeHash == 0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012) {
208+ return chainId == 130 || chainId == 1868 || chainId == 57_073; // unichain, soneium, ink
209+ }
163210
164211 // Taiko, 3204 bytes.
165- if (codeHash == 0x9f3d95086909fce850d997158aba31abe26c3aad6a413107ca0bf9d53a7c42e9) return true; // taiko
212+ if (codeHash == 0x9f3d95086909fce850d997158aba31abe26c3aad6a413107ca0bf9d53a7c42e9) {
213+ return chainId == 167_000; // taiko
214+ }
166215
167216 // Scroll, 5871 bytes.
168- if (codeHash == 0xe8c4073351c26b9831c1e5af153b9be4713a4af9edfdf32b58077b735e120f14) return true; // scroll
217+ if (codeHash == 0xe8c4073351c26b9831c1e5af153b9be4713a4af9edfdf32b58077b735e120f14) {
218+ return chainId == 534_352; // scroll
219+ }
169220
170221 return false;
171222 }
@@ -17,15 +17,23 @@ import {IWETH} from "./IWETH.sol";
17 ///17 ///
18 /// Two gates run at registration:18 /// Two gates run at registration:
19 ///19 ///
20-/// 1. `EXTCODEHASH` against the allowlist below. This is the real20+/// 1. `EXTCODEHASH` against the allowlist below, paired with the current
21-/// defence: matching means the candidate is provably one of a handful21+/// `block.chainid`. This is the real defence: matching means the
22-/// of implementations whose source has been read, not merely something22+/// candidate is provably one of a handful of implementations whose
23-/// that behaves well today. It is what catches a hidden mint, an23+/// source has been read *and reviewed for this chain* — not merely
24-/// upgrade hook or a backdoor, none of which a behavioural test can24+/// something that behaves well today, and not merely bytecode that
25-/// see.25+/// happens to be byte-identical to something reviewed elsewhere.
26+/// Pairing with the chain ID is what stops a chain-agnostic
27+/// implementation like canonical WETH9 — no constructor argument, no
28+/// chain-specific logic, so its bytecode is identical everywhere —
29+/// from being accepted on a chain whose native currency isn't ETH at
30+/// all, where it would still wrap correctly, just the wrong asset.
26 /// 2. A one-wei deposit/withdraw round trip, which catches the plain31 /// 2. A one-wei deposit/withdraw round trip, which catches the plain
27 /// mistake of an address that is not wETH at all, and proves the32 /// mistake of an address that is not wETH at all, and proves the
28-/// wrapper actually works on this chain.33+/// wrapper actually works on this chain. It cannot, by itself, prove
34+/// the native currency being wrapped is ETH: that fact isn't
35+/// observable from inside the EVM, which is exactly why gate 1 has to
36+/// carry the chain ID.
29 ///37 ///
30 /// The registration is write-once. There is no setter, no owner and no38 /// The registration is write-once. There is no setter, no owner and no
31 /// way to revoke an entry, so a `Love` deployed against this registry can39 /// way to revoke an entry, so a `Love` deployed against this registry can
@@ -61,10 +69,11 @@ contract WETHRegistry {
61 /// @param registered The wETH registered by the earlier call.69 /// @param registered The wETH registered by the earlier call.
62 error AlreadyRegistered(IWETH registered);70 error AlreadyRegistered(IWETH registered);
63 71
64- /// @notice Thrown when the candidate's code is not a reviewed wETH.72+ /// @notice Thrown when the candidate's code is not a reviewed wETH on this chain.
65 /// @param candidate The rejected address.73 /// @param candidate The rejected address.
66 /// @param codeHash Its `EXTCODEHASH`, zero if there is no code there.74 /// @param codeHash Its `EXTCODEHASH`, zero if there is no code there.
67- error UnknownImplementation(IWETH candidate, bytes32 codeHash);75+ /// @param chainId The chain the candidate was rejected on.
76+ error UnknownImplementation(IWETH candidate, bytes32 codeHash, uint256 chainId);
68 77
69 /// @notice Thrown when the call does not carry exactly `PROBE` wei.78 /// @notice Thrown when the call does not carry exactly `PROBE` wei.
70 /// @param sent The value that came with the call.79 /// @param sent The value that came with the call.
@@ -115,7 +124,9 @@ contract WETHRegistry {
115 if (msg.value != PROBE) revert ProbeValueRequired(msg.value);124 if (msg.value != PROBE) revert ProbeValueRequired(msg.value);
116 125
117 bytes32 codeHash = address(candidate).codehash;126 bytes32 codeHash = address(candidate).codehash;
118- if (!isKnownImplementation(codeHash)) revert UnknownImplementation(candidate, codeHash);127+ if (!isKnownImplementation(codeHash, block.chainid)) {
128+ revert UnknownImplementation(candidate, codeHash, block.chainid);
129+ }
119 130
120 _probe(candidate);131 _probe(candidate);
121 132
@@ -125,7 +136,7 @@ contract WETHRegistry {
125 return candidate;136 return candidate;
126 }137 }
127 138
128- /// @notice Whether `codeHash` is one of the reviewed wETH implementations.139+ /// @notice Whether `codeHash` is a reviewed wETH implementation on `chainId`.
129 /// @dev Compile-time, so the list is ownerless and append-only by140 /// @dev Compile-time, so the list is ownerless and append-only by
130 /// construction: extending it means publishing a new registry, which141 /// construction: extending it means publishing a new registry, which
131 /// leaves every existing deployment exactly as it was. No key can142 /// leaves every existing deployment exactly as it was. No key can
@@ -140,32 +151,72 @@ contract WETHRegistry {
140 /// holding for every family added later. Ten constants is the cheaper151 /// holding for every family added later. Ten constants is the cheaper
141 /// side of that trade.152 /// side of that trade.
142 ///153 ///
143- /// Grouped by implementation. Every value is reproducible from chain154+ /// Paired with `chainId` rather than accepted on any chain that
144- /// state with `script/weth-codehashes.sh`.155+ /// reproduces the hash: some of these implementations (canonical
156+ /// WETH9 above all — no constructor argument, no reference to
157+ /// anything chain-specific) compile to the exact same bytecode
158+ /// wherever they're deployed. Accepting the hash alone would mean a
159+ /// chain whose native currency isn't ETH could register a candidate
160+ /// that wraps its native currency flawlessly — the round-trip probe
161+ /// would pass — while wrapping the wrong asset entirely, since
162+ /// nothing observable from inside the EVM distinguishes "native
163+ /// currency is ETH" from "native currency behaves like ETH." Pairing
164+ /// the hash with the specific chain IDs it was actually reviewed on
165+ /// closes that gap: a hash match now means "reviewed on this chain,"
166+ /// not "reviewed somewhere."
167+ ///
168+ /// Grouped by implementation. Every codehash is reproducible from
169+ /// chain state with `script/weth-codehashes.sh`; the paired chain IDs
170+ /// are exactly the chains that script read that hash from.
145 /// @param codeHash The `EXTCODEHASH` to check.171 /// @param codeHash The `EXTCODEHASH` to check.
146- /// @return True if a candidate with this code may be registered.172+ /// @param chainId The chain the candidate was found on, i.e. `block.chainid`.
147- function isKnownImplementation(bytes32 codeHash) public pure returns (bool) {173+ /// @return True if a candidate with this code may be registered on this chain.
174+ function isKnownImplementation(bytes32 codeHash, uint256 chainId) public pure returns (bool) {
148 // OP Stack legacy WETH9, 2041 bytes, solc 0.5.17. One implementation,175 // OP Stack legacy WETH9, 2041 bytes, solc 0.5.17. One implementation,
149 // four hashes: these chains run byte-identical code and differ only176 // four hashes: these chains run byte-identical code and differ only
150 // inside solc's metadata blob, which never executes.177 // inside solc's metadata blob, which never executes.
151- if (codeHash == 0x779bbf2a738ef09d961c945116197e2ac764c1b39304b2b4418cd4e42668b173) return true; // optimism178+ if (codeHash == 0x779bbf2a738ef09d961c945116197e2ac764c1b39304b2b4418cd4e42668b173) {
152- if (codeHash == 0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c) return true; // base, mode, zora179+ return chainId == 10; // optimism
153- if (codeHash == 0x557c8e14d33f7cd67cad0141e1a49ebf3488a447fc3df7aa66b127778a0383d1) return true; // world-chain180+ }
154- if (codeHash == 0xf35fe602ba2a3b96f2e27ff7c8b8010800a8d0d616a5fb1f902e087b590355f3) return true; // lisk181+ if (codeHash == 0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c) {
155-182+ return chainId == 8453 || chainId == 34_443 || chainId == 7_777_777; // base, mode, zora
156- // Canonical WETH9, 3124 bytes, solc 0.4.19.183+ }
157- if (codeHash == 0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23) return true; // ethereum184+ if (codeHash == 0x557c8e14d33f7cd67cad0141e1a49ebf3488a447fc3df7aa66b127778a0383d1) {
158- if (codeHash == 0xa670ec6c272ddec6d328d6f3d5cad65a841a6ab45e8e5cf825150eb458be4f1f) return true; // linea185+ return chainId == 480; // world-chain
159- if (codeHash == 0x032e9cab14331328530468e54f1b91777b4d5c9dbbb400884badb32bc4113585) return true; // polygon-zkevm186+ }
187+ if (codeHash == 0xf35fe602ba2a3b96f2e27ff7c8b8010800a8d0d616a5fb1f902e087b590355f3) {
188+ return chainId == 1135; // lisk
189+ }
190+
191+ // Canonical WETH9, 3124 bytes, solc 0.4.19. Chain-agnostic bytecode —
192+ // no constructor argument, nothing chain-specific — so it reproduces
193+ // identically anywhere; the chain ID is the only thing standing
194+ // between "reviewed" and "byte-identical to something reviewed
195+ // elsewhere."
196+ if (codeHash == 0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23) {
197+ return chainId == 1; // ethereum
198+ }
199+ if (codeHash == 0xa670ec6c272ddec6d328d6f3d5cad65a841a6ab45e8e5cf825150eb458be4f1f) {
200+ return chainId == 59_144; // linea
201+ }
202+ if (codeHash == 0x032e9cab14331328530468e54f1b91777b4d5c9dbbb400884badb32bc4113585) {
203+ return chainId == 1101; // polygon-zkevm
204+ }
160 205
161 // OP Stack WETH, 2865 bytes, solc 0.8.15.206 // OP Stack WETH, 2865 bytes, solc 0.8.15.
162- if (codeHash == 0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012) return true; // unichain, soneium, ink207+ if (codeHash == 0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012) {
208+ return chainId == 130 || chainId == 1868 || chainId == 57_073; // unichain, soneium, ink
209+ }
163 210
164 // Taiko, 3204 bytes.211 // Taiko, 3204 bytes.
165- if (codeHash == 0x9f3d95086909fce850d997158aba31abe26c3aad6a413107ca0bf9d53a7c42e9) return true; // taiko212+ if (codeHash == 0x9f3d95086909fce850d997158aba31abe26c3aad6a413107ca0bf9d53a7c42e9) {
213+ return chainId == 167_000; // taiko
214+ }
166 215
167 // Scroll, 5871 bytes.216 // Scroll, 5871 bytes.
168- if (codeHash == 0xe8c4073351c26b9831c1e5af153b9be4713a4af9edfdf32b58077b735e120f14) return true; // scroll217+ if (codeHash == 0xe8c4073351c26b9831c1e5af153b9be4713a4af9edfdf32b58077b735e120f14) {
218+ return chainId == 534_352; // scroll
219+ }
169 220
170 return false;221 return false;
171 }222 }
modified test/Fixtures.sol +19 -1
@@ -22,9 +22,17 @@ abstract contract Fixtures is CommonBase, StdCheats {
2222 /// @notice OP Stack legacy WETH9, as deployed on Base, Mode and Zora.
2323 string internal constant WETH9_OP_LEGACY = "test/fixtures/weth9-op-legacy.hex";
2424
25+ /// @notice A chain WETH9_OP_LEGACY's codehash is reviewed for.
26+ /// @dev Base; Mode and Zora share the same codehash and would do equally
27+ /// well here, since `isKnownImplementation` accepts all three.
28+ uint256 internal constant WETH9_OP_LEGACY_CHAIN_ID = 8453;
29+
2530 /// @notice Canonical WETH9, as deployed on Ethereum mainnet.
2631 string internal constant WETH9_CANONICAL = "test/fixtures/weth9-canonical.hex";
2732
33+ /// @notice The chain WETH9_CANONICAL's codehash is reviewed for.
34+ uint256 internal constant WETH9_CANONICAL_CHAIN_ID = 1;
35+
2836 /// @notice Put real wETH runtime bytecode at `where`.
2937 /// @dev Reads the fixture rather than embedding it, so refreshing a
3038 /// fixture from chain state does not mean editing Solidity.
@@ -66,11 +74,21 @@ abstract contract Fixtures is CommonBase, StdCheats {
6674 }
6775
6876 /// @notice The whole chain in one call: registry, wETH, registration.
77+ /// @dev Sets `block.chainid` to `chainId` first — the registry now checks
78+ /// the candidate's codehash against the chain it's being registered
79+ /// on, not just against the allowlist in isolation, so a test has to
80+ /// put itself on a chain the fixture's codehash is actually reviewed
81+ /// for. Use one of the `WETH9_*_CHAIN_ID` constants above.
6982 /// @param wethAt The address to etch wETH at.
7083 /// @param fixture Which implementation to etch.
84+ /// @param chainId The chain to register on; see `WETH9_*_CHAIN_ID`.
7185 /// @return registry The registry, with `wethAt` registered.
7286 /// @return weth The wETH now backing any `Love` deployed here.
73- function setUpChain(address wethAt, string memory fixture) internal returns (WETHRegistry registry, IWETH weth) {
87+ function setUpChain(address wethAt, string memory fixture, uint256 chainId)
88+ internal
89+ returns (WETHRegistry registry, IWETH weth)
90+ {
91+ vm.chainId(chainId);
7492 registry = deployRegistry();
7593 weth = etchWeth(wethAt, fixture);
7694 register(registry, weth);
@@ -22,9 +22,17 @@ abstract contract Fixtures is CommonBase, StdCheats {
22 /// @notice OP Stack legacy WETH9, as deployed on Base, Mode and Zora.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";23 string internal constant WETH9_OP_LEGACY = "test/fixtures/weth9-op-legacy.hex";
24 24
25+ /// @notice A chain WETH9_OP_LEGACY's codehash is reviewed for.
26+ /// @dev Base; Mode and Zora share the same codehash and would do equally
27+ /// well here, since `isKnownImplementation` accepts all three.
28+ uint256 internal constant WETH9_OP_LEGACY_CHAIN_ID = 8453;
29+
25 /// @notice Canonical WETH9, as deployed on Ethereum mainnet.30 /// @notice Canonical WETH9, as deployed on Ethereum mainnet.
26 string internal constant WETH9_CANONICAL = "test/fixtures/weth9-canonical.hex";31 string internal constant WETH9_CANONICAL = "test/fixtures/weth9-canonical.hex";
27 32
33+ /// @notice The chain WETH9_CANONICAL's codehash is reviewed for.
34+ uint256 internal constant WETH9_CANONICAL_CHAIN_ID = 1;
35+
28 /// @notice Put real wETH runtime bytecode at `where`.36 /// @notice Put real wETH runtime bytecode at `where`.
29 /// @dev Reads the fixture rather than embedding it, so refreshing a37 /// @dev Reads the fixture rather than embedding it, so refreshing a
30 /// fixture from chain state does not mean editing Solidity.38 /// fixture from chain state does not mean editing Solidity.
@@ -66,11 +74,21 @@ abstract contract Fixtures is CommonBase, StdCheats {
66 }74 }
67 75
68 /// @notice The whole chain in one call: registry, wETH, registration.76 /// @notice The whole chain in one call: registry, wETH, registration.
77+ /// @dev Sets `block.chainid` to `chainId` first — the registry now checks
78+ /// the candidate's codehash against the chain it's being registered
79+ /// on, not just against the allowlist in isolation, so a test has to
80+ /// put itself on a chain the fixture's codehash is actually reviewed
81+ /// for. Use one of the `WETH9_*_CHAIN_ID` constants above.
69 /// @param wethAt The address to etch wETH at.82 /// @param wethAt The address to etch wETH at.
70 /// @param fixture Which implementation to etch.83 /// @param fixture Which implementation to etch.
84+ /// @param chainId The chain to register on; see `WETH9_*_CHAIN_ID`.
71 /// @return registry The registry, with `wethAt` registered.85 /// @return registry The registry, with `wethAt` registered.
72 /// @return weth The wETH now backing any `Love` deployed here.86 /// @return weth The wETH now backing any `Love` deployed here.
73- function setUpChain(address wethAt, string memory fixture) internal returns (WETHRegistry registry, IWETH weth) {87+ function setUpChain(address wethAt, string memory fixture, uint256 chainId)
88+ internal
89+ returns (WETHRegistry registry, IWETH weth)
90+ {
91+ vm.chainId(chainId);
74 registry = deployRegistry();92 registry = deployRegistry();
75 weth = etchWeth(wethAt, fixture);93 weth = etchWeth(wethAt, fixture);
76 register(registry, weth);94 register(registry, weth);
modified test/Love.t.sol +1 -1
@@ -29,7 +29,7 @@ contract LoveTest is Test, Fixtures {
2929 address constant WETH_AT = 0x4200000000000000000000000000000000000006;
3030
3131 function setUp() public {
32- (, weth) = setUpChain(WETH_AT, WETH9_OP_LEGACY);
32+ (, weth) = setUpChain(WETH_AT, WETH9_OP_LEGACY, WETH9_OP_LEGACY_CHAIN_ID);
3333 love = new Love();
3434
3535 _fund(alice, 10 ether);
@@ -29,7 +29,7 @@ contract LoveTest is Test, Fixtures {
29 address constant WETH_AT = 0x4200000000000000000000000000000000000006;29 address constant WETH_AT = 0x4200000000000000000000000000000000000006;
30 30
31 function setUp() public {31 function setUp() public {
32- (, weth) = setUpChain(WETH_AT, WETH9_OP_LEGACY);32+ (, weth) = setUpChain(WETH_AT, WETH9_OP_LEGACY, WETH9_OP_LEGACY_CHAIN_ID);
33 love = new Love();33 love = new Love();
34 34
35 _fund(alice, 10 ether);35 _fund(alice, 10 ether);
modified test/LoveCreate2.t.sol +7 -8
@@ -36,7 +36,7 @@ contract LoveCreate2Test is Test, Fixtures {
3636 /// change of optimizer settings — this fails rather than shipping a
3737 /// token pointing at an empty address.
3838 function test_LoveRegistryConstantMatchesDeterministicAddress() public {
39- setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY);
39+ setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY, WETH9_OP_LEGACY_CHAIN_ID);
4040
4141 assertEq(new Love().REGISTRY(), script.registryAddress());
4242 }
@@ -62,7 +62,7 @@ contract LoveCreate2Test is Test, Fixtures {
6262 //////////////////////////////////////////////////////////////*/
6363
6464 function test_DeploysAtPredictedAddress() public {
65- setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY);
65+ setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY, WETH9_OP_LEGACY_CHAIN_ID);
6666
6767 address predicted = vm.computeCreate2Address(SALT, keccak256(script.initCode()), CREATE2_FACTORY);
6868 Love deployed = Love(_deploy(SALT));
@@ -74,7 +74,7 @@ contract LoveCreate2Test is Test, Fixtures {
7474
7575 /// @dev What the deploy script prints must be what the chain gives back.
7676 function test_ScriptPredictionMatchesDeployment() public {
77- setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY);
77+ setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY, WETH9_OP_LEGACY_CHAIN_ID);
7878
7979 assertEq(script.predict(SALT), _deploy(SALT));
8080 }
@@ -116,7 +116,7 @@ contract LoveCreate2Test is Test, Fixtures {
116116 // to build over that, so the registry could never be redeployed.
117117 uint256 freshChain = vm.snapshotState();
118118
119- setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY);
119+ setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY, WETH9_OP_LEGACY_CHAIN_ID);
120120 address onOpStack = _deploy(SALT);
121121 assertEq(address(Love(onOpStack).WETH()), OP_STACK_WETH);
122122
@@ -124,8 +124,7 @@ contract LoveCreate2Test is Test, Fixtures {
124124
125125 // Different chain id, a different wETH implementation, at a different
126126 // address.
127- vm.chainId(1);
128- setUpChain(MAINNET_WETH, WETH9_CANONICAL);
127+ setUpChain(MAINNET_WETH, WETH9_CANONICAL, WETH9_CANONICAL_CHAIN_ID);
129128 address onMainnet = _deploy(SALT);
130129 assertEq(address(Love(onMainnet).WETH()), MAINNET_WETH);
131130
@@ -135,7 +134,7 @@ contract LoveCreate2Test is Test, Fixtures {
135134 /// @dev Redeploying with the same salt must fail, not silently return the
136135 /// existing token.
137136 function test_RevertWhen_RedeployingWithSameSalt() public {
138- setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY);
137+ setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY, WETH9_OP_LEGACY_CHAIN_ID);
139138 _deploy(SALT);
140139
141140 (bool ok,) = CREATE2_FACTORY.call(abi.encodePacked(SALT, script.initCode()));
@@ -143,7 +142,7 @@ contract LoveCreate2Test is Test, Fixtures {
143142 }
144143
145144 function testFuzz_PredictionMatchesDeployment(bytes32 salt) public {
146- setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY);
145+ setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY, WETH9_OP_LEGACY_CHAIN_ID);
147146
148147 assertEq(script.predict(salt), _deploy(salt));
149148 }
@@ -36,7 +36,7 @@ contract LoveCreate2Test is Test, Fixtures {
36 /// change of optimizer settings — this fails rather than shipping a36 /// change of optimizer settings — this fails rather than shipping a
37 /// token pointing at an empty address.37 /// token pointing at an empty address.
38 function test_LoveRegistryConstantMatchesDeterministicAddress() public {38 function test_LoveRegistryConstantMatchesDeterministicAddress() public {
39- setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY);39+ setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY, WETH9_OP_LEGACY_CHAIN_ID);
40 40
41 assertEq(new Love().REGISTRY(), script.registryAddress());41 assertEq(new Love().REGISTRY(), script.registryAddress());
42 }42 }
@@ -62,7 +62,7 @@ contract LoveCreate2Test is Test, Fixtures {
62 //////////////////////////////////////////////////////////////*/62 //////////////////////////////////////////////////////////////*/
63 63
64 function test_DeploysAtPredictedAddress() public {64 function test_DeploysAtPredictedAddress() public {
65- setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY);65+ setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY, WETH9_OP_LEGACY_CHAIN_ID);
66 66
67 address predicted = vm.computeCreate2Address(SALT, keccak256(script.initCode()), CREATE2_FACTORY);67 address predicted = vm.computeCreate2Address(SALT, keccak256(script.initCode()), CREATE2_FACTORY);
68 Love deployed = Love(_deploy(SALT));68 Love deployed = Love(_deploy(SALT));
@@ -74,7 +74,7 @@ contract LoveCreate2Test is Test, Fixtures {
74 74
75 /// @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.
76 function test_ScriptPredictionMatchesDeployment() public {76 function test_ScriptPredictionMatchesDeployment() public {
77- setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY);77+ setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY, WETH9_OP_LEGACY_CHAIN_ID);
78 78
79 assertEq(script.predict(SALT), _deploy(SALT));79 assertEq(script.predict(SALT), _deploy(SALT));
80 }80 }
@@ -116,7 +116,7 @@ contract LoveCreate2Test is Test, Fixtures {
116 // to build over that, so the registry could never be redeployed.116 // to build over that, so the registry could never be redeployed.
117 uint256 freshChain = vm.snapshotState();117 uint256 freshChain = vm.snapshotState();
118 118
119- setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY);119+ setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY, WETH9_OP_LEGACY_CHAIN_ID);
120 address onOpStack = _deploy(SALT);120 address onOpStack = _deploy(SALT);
121 assertEq(address(Love(onOpStack).WETH()), OP_STACK_WETH);121 assertEq(address(Love(onOpStack).WETH()), OP_STACK_WETH);
122 122
@@ -124,8 +124,7 @@ contract LoveCreate2Test is Test, Fixtures {
124 124
125 // Different chain id, a different wETH implementation, at a different125 // Different chain id, a different wETH implementation, at a different
126 // address.126 // address.
127- vm.chainId(1);127+ setUpChain(MAINNET_WETH, WETH9_CANONICAL, WETH9_CANONICAL_CHAIN_ID);
128- setUpChain(MAINNET_WETH, WETH9_CANONICAL);
129 address onMainnet = _deploy(SALT);128 address onMainnet = _deploy(SALT);
130 assertEq(address(Love(onMainnet).WETH()), MAINNET_WETH);129 assertEq(address(Love(onMainnet).WETH()), MAINNET_WETH);
131 130
@@ -135,7 +134,7 @@ contract LoveCreate2Test is Test, Fixtures {
135 /// @dev Redeploying with the same salt must fail, not silently return the134 /// @dev Redeploying with the same salt must fail, not silently return the
136 /// existing token.135 /// existing token.
137 function test_RevertWhen_RedeployingWithSameSalt() public {136 function test_RevertWhen_RedeployingWithSameSalt() public {
138- setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY);137+ setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY, WETH9_OP_LEGACY_CHAIN_ID);
139 _deploy(SALT);138 _deploy(SALT);
140 139
141 (bool ok,) = CREATE2_FACTORY.call(abi.encodePacked(SALT, script.initCode()));140 (bool ok,) = CREATE2_FACTORY.call(abi.encodePacked(SALT, script.initCode()));
@@ -143,7 +142,7 @@ contract LoveCreate2Test is Test, Fixtures {
143 }142 }
144 143
145 function testFuzz_PredictionMatchesDeployment(bytes32 salt) public {144 function testFuzz_PredictionMatchesDeployment(bytes32 salt) public {
146- setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY);145+ setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY, WETH9_OP_LEGACY_CHAIN_ID);
147 146
148 assertEq(script.predict(salt), _deploy(salt));147 assertEq(script.predict(salt), _deploy(salt));
149 }148 }
modified test/LoveEnvelope.t.sol +1 -1
@@ -29,7 +29,7 @@ contract LoveEnvelopeTest is Test, Fixtures {
2929 address constant WETH_AT = 0x4200000000000000000000000000000000000006;
3030
3131 function setUp() public {
32- (, weth) = setUpChain(WETH_AT, WETH9_OP_LEGACY);
32+ (, weth) = setUpChain(WETH_AT, WETH9_OP_LEGACY, WETH9_OP_LEGACY_CHAIN_ID);
3333 love = new Love();
3434
3535 _fund(alice, 10 ether);
@@ -29,7 +29,7 @@ contract LoveEnvelopeTest is Test, Fixtures {
29 address constant WETH_AT = 0x4200000000000000000000000000000000000006;29 address constant WETH_AT = 0x4200000000000000000000000000000000000006;
30 30
31 function setUp() public {31 function setUp() public {
32- (, weth) = setUpChain(WETH_AT, WETH9_OP_LEGACY);32+ (, weth) = setUpChain(WETH_AT, WETH9_OP_LEGACY, WETH9_OP_LEGACY_CHAIN_ID);
33 love = new Love();33 love = new Love();
34 34
35 _fund(alice, 10 ether);35 _fund(alice, 10 ether);
modified test/WETHRegistry.t.sol +110 -32
@@ -33,6 +33,9 @@ contract WETHRegistryTest is Test, Fixtures {
3333 event Registered(IWETH indexed weth, address indexed registrar, bytes32 codeHash);
3434
3535 function setUp() public {
36+ // Ambient default: every test below registers WETH9_OP_LEGACY unless
37+ // it says otherwise, so put the chain where that codehash is reviewed.
38+ vm.chainId(WETH9_OP_LEGACY_CHAIN_ID);
3639 registry = deployRegistry();
3740 probe = registry.PROBE();
3841 }
@@ -41,42 +44,109 @@ contract WETHRegistryTest is Test, Fixtures {
4144 THE ALLOWLIST
4245 //////////////////////////////////////////////////////////////*/
4346
44- /// @dev The ten entries, reproducible from chain state with
45- /// `script/weth-codehashes.sh`. Listing them here means a hash edited
46- /// in the contract shows up as a failing test rather than as a silent
47- /// change to what LOVE will accept.
47+ /// @dev The ten hashes, each paired with every chain ID it's actually
48+ /// reviewed for, reproducible from chain state with
49+ /// `script/weth-codehashes.sh`. Listing them here means a hash or a
50+ /// chain ID edited in the contract shows up as a failing test rather
51+ /// than as a silent change to what LOVE will accept.
4852 function test_KnownImplementations() public view {
49- bytes32[10] memory allowed = [
50- // OP Stack legacy WETH9
51- bytes32(0x779bbf2a738ef09d961c945116197e2ac764c1b39304b2b4418cd4e42668b173),
52- 0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c,
53- 0x557c8e14d33f7cd67cad0141e1a49ebf3488a447fc3df7aa66b127778a0383d1,
54- 0xf35fe602ba2a3b96f2e27ff7c8b8010800a8d0d616a5fb1f902e087b590355f3,
55- // Canonical WETH9
56- 0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23,
57- 0xa670ec6c272ddec6d328d6f3d5cad65a841a6ab45e8e5cf825150eb458be4f1f,
58- 0x032e9cab14331328530468e54f1b91777b4d5c9dbbb400884badb32bc4113585,
59- // OP Stack WETH, solc 0.8.15
60- 0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012,
61- // Taiko
62- 0x9f3d95086909fce850d997158aba31abe26c3aad6a413107ca0bf9d53a7c42e9,
63- // Scroll
64- 0xe8c4073351c26b9831c1e5af153b9be4713a4af9edfdf32b58077b735e120f14
65- ];
66-
67- for (uint256 i = 0; i < allowed.length; i++) {
68- assertTrue(registry.isKnownImplementation(allowed[i]), "allowlisted hash rejected");
69- }
53+ assertTrue(
54+ registry.isKnownImplementation(0x779bbf2a738ef09d961c945116197e2ac764c1b39304b2b4418cd4e42668b173, 10),
55+ "optimism"
56+ );
57+ assertTrue(
58+ registry.isKnownImplementation(0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c, 8453),
59+ "base"
60+ );
61+ assertTrue(
62+ registry.isKnownImplementation(0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c, 34_443),
63+ "mode"
64+ );
65+ assertTrue(
66+ registry.isKnownImplementation(
67+ 0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c, 7_777_777
68+ ),
69+ "zora"
70+ );
71+ assertTrue(
72+ registry.isKnownImplementation(0x557c8e14d33f7cd67cad0141e1a49ebf3488a447fc3df7aa66b127778a0383d1, 480),
73+ "world-chain"
74+ );
75+ assertTrue(
76+ registry.isKnownImplementation(0xf35fe602ba2a3b96f2e27ff7c8b8010800a8d0d616a5fb1f902e087b590355f3, 1135),
77+ "lisk"
78+ );
79+ assertTrue(
80+ registry.isKnownImplementation(0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23, 1),
81+ "ethereum"
82+ );
83+ assertTrue(
84+ registry.isKnownImplementation(0xa670ec6c272ddec6d328d6f3d5cad65a841a6ab45e8e5cf825150eb458be4f1f, 59_144),
85+ "linea"
86+ );
87+ assertTrue(
88+ registry.isKnownImplementation(0x032e9cab14331328530468e54f1b91777b4d5c9dbbb400884badb32bc4113585, 1101),
89+ "polygon-zkevm"
90+ );
91+ assertTrue(
92+ registry.isKnownImplementation(0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012, 130),
93+ "unichain"
94+ );
95+ assertTrue(
96+ registry.isKnownImplementation(0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012, 1868),
97+ "soneium"
98+ );
99+ assertTrue(
100+ registry.isKnownImplementation(0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012, 57_073),
101+ "ink"
102+ );
103+ assertTrue(
104+ registry.isKnownImplementation(0x9f3d95086909fce850d997158aba31abe26c3aad6a413107ca0bf9d53a7c42e9, 167_000),
105+ "taiko"
106+ );
107+ assertTrue(
108+ registry.isKnownImplementation(0xe8c4073351c26b9831c1e5af153b9be4713a4af9edfdf32b58077b735e120f14, 534_352),
109+ "scroll"
110+ );
70111 }
71112
72113 function test_EmptyCodeHashIsNotKnown() public view {
73- assertFalse(registry.isKnownImplementation(bytes32(0)));
74- assertFalse(registry.isKnownImplementation(keccak256("")));
114+ assertFalse(registry.isKnownImplementation(bytes32(0), WETH9_OP_LEGACY_CHAIN_ID));
115+ assertFalse(registry.isKnownImplementation(keccak256(""), WETH9_OP_LEGACY_CHAIN_ID));
116+ }
117+
118+ function testFuzz_UnknownHashesAreRejected(bytes32 codeHash, uint256 chainId) public view {
119+ vm.assume(!registry.isKnownImplementation(codeHash, chainId));
120+ assertFalse(registry.isKnownImplementation(codeHash, chainId));
75121 }
76122
77- function testFuzz_UnknownHashesAreRejected(bytes32 codeHash) public view {
78- vm.assume(!registry.isKnownImplementation(codeHash));
79- assertFalse(registry.isKnownImplementation(codeHash));
123+ /// @dev The gap this whole change closes: canonical WETH9's bytecode
124+ /// carries no dependency on which chain it runs on, so a codehash
125+ /// match by itself would say nothing about whether the native
126+ /// currency behind it is actually ETH. 137 is Polygon PoS, whose
127+ /// native token is POL — canonical WETH9's hash was never reviewed
128+ /// there, so it must not pass here even though it passes on chain 1.
129+ function test_RevertWhen_ReviewedHashOnWrongChain() public view {
130+ bytes32 canonicalHash = 0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23;
131+ assertTrue(registry.isKnownImplementation(canonicalHash, 1), "sanity: valid on ethereum");
132+ assertFalse(registry.isKnownImplementation(canonicalHash, 137), "must not validate on polygon pos");
133+ }
134+
135+ /// @dev End-to-end version of the above: the exact same candidate
136+ /// contract, byte-identical bytecode, registers on the chain its
137+ /// codehash was reviewed for and is rejected on one it wasn't —
138+ /// Native Currency Substitution (docs/notes/native-currency-substitution.md).
139+ function test_RevertWhen_RegisteringCanonicalWethOnWrongChain() public {
140+ IWETH weth = etchWeth(OTHER_WETH_AT, WETH9_CANONICAL);
141+ deal(address(this), 1 ether);
142+
143+ vm.chainId(137); // polygon pos, native token POL, never reviewed for this hash
144+ vm.expectRevert(
145+ abi.encodeWithSelector(
146+ WETHRegistry.UnknownImplementation.selector, IWETH(OTHER_WETH_AT), OTHER_WETH_AT.codehash, uint256(137)
147+ )
148+ );
149+ registry.register{value: probe}(weth);
80150 }
81151
82152 /*//////////////////////////////////////////////////////////////
@@ -91,6 +161,7 @@ contract WETHRegistryTest is Test, Fixtures {
91161 }
92162
93163 function test_RegistersCanonicalWeth() public {
164+ vm.chainId(WETH9_CANONICAL_CHAIN_ID);
94165 IWETH weth = etchWeth(OTHER_WETH_AT, WETH9_CANONICAL);
95166 register(registry, weth);
96167
@@ -144,7 +215,10 @@ contract WETHRegistryTest is Test, Fixtures {
144215
145216 vm.expectRevert(
146217 abi.encodeWithSelector(
147- WETHRegistry.UnknownImplementation.selector, IWETH(address(mock)), address(mock).codehash
218+ WETHRegistry.UnknownImplementation.selector,
219+ IWETH(address(mock)),
220+ address(mock).codehash,
221+ WETH9_OP_LEGACY_CHAIN_ID
148222 )
149223 );
150224 registry.register{value: probe}(IWETH(address(mock)));
@@ -154,7 +228,11 @@ contract WETHRegistryTest is Test, Fixtures {
154228 address empty = makeAddr("empty");
155229 deal(address(this), 1 ether);
156230
157- vm.expectRevert(abi.encodeWithSelector(WETHRegistry.UnknownImplementation.selector, IWETH(empty), bytes32(0)));
231+ vm.expectRevert(
232+ abi.encodeWithSelector(
233+ WETHRegistry.UnknownImplementation.selector, IWETH(empty), bytes32(0), WETH9_OP_LEGACY_CHAIN_ID
234+ )
235+ );
158236 registry.register{value: probe}(IWETH(empty));
159237 }
160238
@@ -33,6 +33,9 @@ contract WETHRegistryTest is Test, Fixtures {
33 event Registered(IWETH indexed weth, address indexed registrar, bytes32 codeHash);33 event Registered(IWETH indexed weth, address indexed registrar, bytes32 codeHash);
34 34
35 function setUp() public {35 function setUp() public {
36+ // Ambient default: every test below registers WETH9_OP_LEGACY unless
37+ // it says otherwise, so put the chain where that codehash is reviewed.
38+ vm.chainId(WETH9_OP_LEGACY_CHAIN_ID);
36 registry = deployRegistry();39 registry = deployRegistry();
37 probe = registry.PROBE();40 probe = registry.PROBE();
38 }41 }
@@ -41,42 +44,109 @@ contract WETHRegistryTest is Test, Fixtures {
41 THE ALLOWLIST44 THE ALLOWLIST
42 //////////////////////////////////////////////////////////////*/45 //////////////////////////////////////////////////////////////*/
43 46
44- /// @dev The ten entries, reproducible from chain state with47+ /// @dev The ten hashes, each paired with every chain ID it's actually
45- /// `script/weth-codehashes.sh`. Listing them here means a hash edited48+ /// reviewed for, reproducible from chain state with
46- /// in the contract shows up as a failing test rather than as a silent49+ /// `script/weth-codehashes.sh`. Listing them here means a hash or a
47- /// change to what LOVE will accept.50+ /// chain ID edited in the contract shows up as a failing test rather
51+ /// than as a silent change to what LOVE will accept.
48 function test_KnownImplementations() public view {52 function test_KnownImplementations() public view {
49- bytes32[10] memory allowed = [53+ assertTrue(
50- // OP Stack legacy WETH954+ registry.isKnownImplementation(0x779bbf2a738ef09d961c945116197e2ac764c1b39304b2b4418cd4e42668b173, 10),
51- bytes32(0x779bbf2a738ef09d961c945116197e2ac764c1b39304b2b4418cd4e42668b173),55+ "optimism"
52- 0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c,56+ );
53- 0x557c8e14d33f7cd67cad0141e1a49ebf3488a447fc3df7aa66b127778a0383d1,57+ assertTrue(
54- 0xf35fe602ba2a3b96f2e27ff7c8b8010800a8d0d616a5fb1f902e087b590355f3,58+ registry.isKnownImplementation(0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c, 8453),
55- // Canonical WETH959+ "base"
56- 0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23,60+ );
57- 0xa670ec6c272ddec6d328d6f3d5cad65a841a6ab45e8e5cf825150eb458be4f1f,61+ assertTrue(
58- 0x032e9cab14331328530468e54f1b91777b4d5c9dbbb400884badb32bc4113585,62+ registry.isKnownImplementation(0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c, 34_443),
59- // OP Stack WETH, solc 0.8.1563+ "mode"
60- 0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012,64+ );
61- // Taiko65+ assertTrue(
62- 0x9f3d95086909fce850d997158aba31abe26c3aad6a413107ca0bf9d53a7c42e9,66+ registry.isKnownImplementation(
63- // Scroll67+ 0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c, 7_777_777
64- 0xe8c4073351c26b9831c1e5af153b9be4713a4af9edfdf32b58077b735e120f1468+ ),
65- ];69+ "zora"
66-70+ );
67- for (uint256 i = 0; i < allowed.length; i++) {71+ assertTrue(
68- assertTrue(registry.isKnownImplementation(allowed[i]), "allowlisted hash rejected");72+ registry.isKnownImplementation(0x557c8e14d33f7cd67cad0141e1a49ebf3488a447fc3df7aa66b127778a0383d1, 480),
69- }73+ "world-chain"
74+ );
75+ assertTrue(
76+ registry.isKnownImplementation(0xf35fe602ba2a3b96f2e27ff7c8b8010800a8d0d616a5fb1f902e087b590355f3, 1135),
77+ "lisk"
78+ );
79+ assertTrue(
80+ registry.isKnownImplementation(0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23, 1),
81+ "ethereum"
82+ );
83+ assertTrue(
84+ registry.isKnownImplementation(0xa670ec6c272ddec6d328d6f3d5cad65a841a6ab45e8e5cf825150eb458be4f1f, 59_144),
85+ "linea"
86+ );
87+ assertTrue(
88+ registry.isKnownImplementation(0x032e9cab14331328530468e54f1b91777b4d5c9dbbb400884badb32bc4113585, 1101),
89+ "polygon-zkevm"
90+ );
91+ assertTrue(
92+ registry.isKnownImplementation(0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012, 130),
93+ "unichain"
94+ );
95+ assertTrue(
96+ registry.isKnownImplementation(0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012, 1868),
97+ "soneium"
98+ );
99+ assertTrue(
100+ registry.isKnownImplementation(0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012, 57_073),
101+ "ink"
102+ );
103+ assertTrue(
104+ registry.isKnownImplementation(0x9f3d95086909fce850d997158aba31abe26c3aad6a413107ca0bf9d53a7c42e9, 167_000),
105+ "taiko"
106+ );
107+ assertTrue(
108+ registry.isKnownImplementation(0xe8c4073351c26b9831c1e5af153b9be4713a4af9edfdf32b58077b735e120f14, 534_352),
109+ "scroll"
110+ );
70 }111 }
71 112
72 function test_EmptyCodeHashIsNotKnown() public view {113 function test_EmptyCodeHashIsNotKnown() public view {
73- assertFalse(registry.isKnownImplementation(bytes32(0)));114+ assertFalse(registry.isKnownImplementation(bytes32(0), WETH9_OP_LEGACY_CHAIN_ID));
74- assertFalse(registry.isKnownImplementation(keccak256("")));115+ assertFalse(registry.isKnownImplementation(keccak256(""), WETH9_OP_LEGACY_CHAIN_ID));
116+ }
117+
118+ function testFuzz_UnknownHashesAreRejected(bytes32 codeHash, uint256 chainId) public view {
119+ vm.assume(!registry.isKnownImplementation(codeHash, chainId));
120+ assertFalse(registry.isKnownImplementation(codeHash, chainId));
75 }121 }
76 122
77- function testFuzz_UnknownHashesAreRejected(bytes32 codeHash) public view {123+ /// @dev The gap this whole change closes: canonical WETH9's bytecode
78- vm.assume(!registry.isKnownImplementation(codeHash));124+ /// carries no dependency on which chain it runs on, so a codehash
79- assertFalse(registry.isKnownImplementation(codeHash));125+ /// match by itself would say nothing about whether the native
126+ /// currency behind it is actually ETH. 137 is Polygon PoS, whose
127+ /// native token is POL — canonical WETH9's hash was never reviewed
128+ /// there, so it must not pass here even though it passes on chain 1.
129+ function test_RevertWhen_ReviewedHashOnWrongChain() public view {
130+ bytes32 canonicalHash = 0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23;
131+ assertTrue(registry.isKnownImplementation(canonicalHash, 1), "sanity: valid on ethereum");
132+ assertFalse(registry.isKnownImplementation(canonicalHash, 137), "must not validate on polygon pos");
133+ }
134+
135+ /// @dev End-to-end version of the above: the exact same candidate
136+ /// contract, byte-identical bytecode, registers on the chain its
137+ /// codehash was reviewed for and is rejected on one it wasn't —
138+ /// Native Currency Substitution (docs/notes/native-currency-substitution.md).
139+ function test_RevertWhen_RegisteringCanonicalWethOnWrongChain() public {
140+ IWETH weth = etchWeth(OTHER_WETH_AT, WETH9_CANONICAL);
141+ deal(address(this), 1 ether);
142+
143+ vm.chainId(137); // polygon pos, native token POL, never reviewed for this hash
144+ vm.expectRevert(
145+ abi.encodeWithSelector(
146+ WETHRegistry.UnknownImplementation.selector, IWETH(OTHER_WETH_AT), OTHER_WETH_AT.codehash, uint256(137)
147+ )
148+ );
149+ registry.register{value: probe}(weth);
80 }150 }
81 151
82 /*//////////////////////////////////////////////////////////////152 /*//////////////////////////////////////////////////////////////
@@ -91,6 +161,7 @@ contract WETHRegistryTest is Test, Fixtures {
91 }161 }
92 162
93 function test_RegistersCanonicalWeth() public {163 function test_RegistersCanonicalWeth() public {
164+ vm.chainId(WETH9_CANONICAL_CHAIN_ID);
94 IWETH weth = etchWeth(OTHER_WETH_AT, WETH9_CANONICAL);165 IWETH weth = etchWeth(OTHER_WETH_AT, WETH9_CANONICAL);
95 register(registry, weth);166 register(registry, weth);
96 167
@@ -144,7 +215,10 @@ contract WETHRegistryTest is Test, Fixtures {
144 215
145 vm.expectRevert(216 vm.expectRevert(
146 abi.encodeWithSelector(217 abi.encodeWithSelector(
147- WETHRegistry.UnknownImplementation.selector, IWETH(address(mock)), address(mock).codehash218+ WETHRegistry.UnknownImplementation.selector,
219+ IWETH(address(mock)),
220+ address(mock).codehash,
221+ WETH9_OP_LEGACY_CHAIN_ID
148 )222 )
149 );223 );
150 registry.register{value: probe}(IWETH(address(mock)));224 registry.register{value: probe}(IWETH(address(mock)));
@@ -154,7 +228,11 @@ contract WETHRegistryTest is Test, Fixtures {
154 address empty = makeAddr("empty");228 address empty = makeAddr("empty");
155 deal(address(this), 1 ether);229 deal(address(this), 1 ether);
156 230
157- vm.expectRevert(abi.encodeWithSelector(WETHRegistry.UnknownImplementation.selector, IWETH(empty), bytes32(0)));231+ vm.expectRevert(
232+ abi.encodeWithSelector(
233+ WETHRegistry.UnknownImplementation.selector, IWETH(empty), bytes32(0), WETH9_OP_LEGACY_CHAIN_ID
234+ )
235+ );
158 registry.register{value: probe}(IWETH(empty));236 registry.register{value: probe}(IWETH(empty));
159 }237 }
160 238