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

Merge 5-weth-bytecode-verification into main

rickub committed 2026-09-17T21:44:47Z Browse files
2f6da5e parent: ce13509
modified .gitignore +2 -2
@@ -8,8 +8,8 @@ out/
88 /broadcast/*/31338/
99 /broadcast/**/dry-run/
1010
11-# Docs
12-docs/
11+# Docs — `forge doc` output only; docs/notes is written by hand and tracked
12+docs/notes
1313
1414 # Dotenv file
1515 .env
@@ -8,8 +8,8 @@ out/
8 /broadcast/*/31338/8 /broadcast/*/31338/
9 /broadcast/**/dry-run/9 /broadcast/**/dry-run/
10 10
11-# Docs11+# Docs — `forge doc` output only; docs/notes is written by hand and tracked
12-docs/12+docs/notes
13 13
14 # Dotenv file14 # Dotenv file
15 .env15 .env
modified CHANGELOG.md +44 -5
@@ -7,8 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
88 ## [Unreleased]
99
10+### Changed
11+
12+- `Love`'s constructor no longer takes wETH. It reads it from `WETHRegistry` at
13+ a fixed address and reverts with `RegistryNotDeployed` or `WethNotRegistered`
14+ if there is nothing to read. The creation code is therefore byte-identical on
15+ every chain, so CREATE2 gives LOVE one address everywhere rather than one per
16+ wETH deployment — the previous behaviour, where the wETH argument was part of
17+ the creation code, is gone. **Breaking:** the deployment interface and the
18+ resulting address both change.
19+- `script/Love.s.sol` brings a chain up in three steps — deploy the registry,
20+ register wETH, deploy the token — skipping whichever has already happened.
21+ `predict()` and `initCode()` no longer take a wETH argument.
22+- `script/multichain-check.sh` now gives its two anvil nodes different wETH
23+ implementations at different addresses, which is what makes a matching LOVE
24+ address meaningful; it also fails if both nodes end up on the same wETH.
25+- Tests run against real WETH9 runtime bytecode etched from live chains rather
26+ than against `MockWETH`, since a mock's codehash is not on the allowlist and
27+ adding it would mean testing a different allowlist from the one that ships.
28+ `MockWETH` is now the "correct behaviour, wrong bytecode" rejection case.
29+
1030 ### Added
1131
32+- `WETHRegistry`, naming the wETH LOVE pegs to on a chain. `register()` is
33+ permissionless and payable, takes exactly 1 wei, and accepts a candidate only
34+ if its `EXTCODEHASH` is one of ten reviewed wETH implementations and a
35+ deposit/withdraw round trip of that wei succeeds. Ownerless, no setter,
36+ write-once, so nothing can move the token under a live peg. The allowlist is
37+ compile-time, so extending it means publishing a new registry and no key can
38+ revoke an entry. The probe wei is kept rather than refunded, so a registrar
39+ with no payable fallback can still register. `receive()` reverts outside a
40+ probe, keeping the registry from becoming ether it cannot move.
41+- `IWETH`, the narrow wrapper interface the registry probes against.
42+- `script/weth-codehashes.sh` and `script/weth-chains.tsv`, surveying the wETH
43+ on 27 chains and grouping them by implementation, so every allowlist entry is
44+ reproducible from chain state. The survey found 23 distinct codehashes across
45+ those chains, collapsing to 5 implementations over 14 chains once bridged-ETH
46+ tokens, upgradeable proxies and non-EVM bytecode are excluded.
47+- `test/fixtures/`, real WETH9 runtime bytecode from Base and Ethereum, with
48+ `test/Fixtures.sol` to stage a chain: registry, wETH, registration.
49+- `WETHRegistryTest`, covering both gates, the write-once rule, the exact probe
50+ value, ether sent outside a probe, and all ten allowlist entries.
1251 - Foundry project scaffolding (`forge-std`, CI workflow, `.gitignore`).
1352 - `Love`, an ERC-20 token (`LOVE`, 18 decimals) on OpenZeppelin v5, pegged to
1453 wETH at 100000 LOVE per wETH. No mint function and no owner: supply moves
@@ -21,12 +60,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2160 approve/`transferFrom`, reverts and fuzzing.
2261 - Deployment script `script/Love.s.sol`, deploying with CREATE2 through the
2362 canonical deterministic deployer; salt defaults to `keccak256("LOVE")` and is
24- overridable via `SALT`, wETH defaults to the OP-Stack address and is
25- overridable via `WETH`. The address embeds the wETH argument, so it is
26- identical across chains that share one wETH deployment.
63+ overridable via `SALT`, the wETH to register defaults to the OP-Stack address
64+ and is overridable via `WETH`. Neither contract takes constructor arguments,
65+ so both land at the same address on every chain.
2766 - `predict()` entrypoint printing the deployment address without broadcasting,
28- and CREATE2 tests pinning address determinism across chain IDs and its
29- dependence on the wETH argument.
67+ and CREATE2 tests pinning address determinism across chain IDs, including
68+ that two chains with different wETH still give LOVE one address.
3069 - `foundry.toml` with pinned solc 0.8.30, optimizer, fmt rules, fuzz/invariant
3170 defaults, and Base Sepolia RPC/Etherscan endpoints driven by env vars.
3271 - `script/multichain-check.sh`, deploying `Love` on two local anvil nodes with
@@ -7,8 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7 7
8 ## [Unreleased]8 ## [Unreleased]
9 9
10+### Changed
11+
12+- `Love`'s constructor no longer takes wETH. It reads it from `WETHRegistry` at
13+ a fixed address and reverts with `RegistryNotDeployed` or `WethNotRegistered`
14+ if there is nothing to read. The creation code is therefore byte-identical on
15+ every chain, so CREATE2 gives LOVE one address everywhere rather than one per
16+ wETH deployment — the previous behaviour, where the wETH argument was part of
17+ the creation code, is gone. **Breaking:** the deployment interface and the
18+ resulting address both change.
19+- `script/Love.s.sol` brings a chain up in three steps — deploy the registry,
20+ register wETH, deploy the token — skipping whichever has already happened.
21+ `predict()` and `initCode()` no longer take a wETH argument.
22+- `script/multichain-check.sh` now gives its two anvil nodes different wETH
23+ implementations at different addresses, which is what makes a matching LOVE
24+ address meaningful; it also fails if both nodes end up on the same wETH.
25+- Tests run against real WETH9 runtime bytecode etched from live chains rather
26+ than against `MockWETH`, since a mock's codehash is not on the allowlist and
27+ adding it would mean testing a different allowlist from the one that ships.
28+ `MockWETH` is now the "correct behaviour, wrong bytecode" rejection case.
29+
10 ### Added30 ### Added
11 31
32+- `WETHRegistry`, naming the wETH LOVE pegs to on a chain. `register()` is
33+ permissionless and payable, takes exactly 1 wei, and accepts a candidate only
34+ if its `EXTCODEHASH` is one of ten reviewed wETH implementations and a
35+ deposit/withdraw round trip of that wei succeeds. Ownerless, no setter,
36+ write-once, so nothing can move the token under a live peg. The allowlist is
37+ compile-time, so extending it means publishing a new registry and no key can
38+ revoke an entry. The probe wei is kept rather than refunded, so a registrar
39+ with no payable fallback can still register. `receive()` reverts outside a
40+ probe, keeping the registry from becoming ether it cannot move.
41+- `IWETH`, the narrow wrapper interface the registry probes against.
42+- `script/weth-codehashes.sh` and `script/weth-chains.tsv`, surveying the wETH
43+ on 27 chains and grouping them by implementation, so every allowlist entry is
44+ reproducible from chain state. The survey found 23 distinct codehashes across
45+ those chains, collapsing to 5 implementations over 14 chains once bridged-ETH
46+ tokens, upgradeable proxies and non-EVM bytecode are excluded.
47+- `test/fixtures/`, real WETH9 runtime bytecode from Base and Ethereum, with
48+ `test/Fixtures.sol` to stage a chain: registry, wETH, registration.
49+- `WETHRegistryTest`, covering both gates, the write-once rule, the exact probe
50+ value, ether sent outside a probe, and all ten allowlist entries.
12 - Foundry project scaffolding (`forge-std`, CI workflow, `.gitignore`).51 - Foundry project scaffolding (`forge-std`, CI workflow, `.gitignore`).
13 - `Love`, an ERC-20 token (`LOVE`, 18 decimals) on OpenZeppelin v5, pegged to52 - `Love`, an ERC-20 token (`LOVE`, 18 decimals) on OpenZeppelin v5, pegged to
14 wETH at 100000 LOVE per wETH. No mint function and no owner: supply moves53 wETH at 100000 LOVE per wETH. No mint function and no owner: supply moves
@@ -21,12 +60,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
21 approve/`transferFrom`, reverts and fuzzing.60 approve/`transferFrom`, reverts and fuzzing.
22 - Deployment script `script/Love.s.sol`, deploying with CREATE2 through the61 - Deployment script `script/Love.s.sol`, deploying with CREATE2 through the
23 canonical deterministic deployer; salt defaults to `keccak256("LOVE")` and is62 canonical deterministic deployer; salt defaults to `keccak256("LOVE")` and is
24- overridable via `SALT`, wETH defaults to the OP-Stack address and is63+ overridable via `SALT`, the wETH to register defaults to the OP-Stack address
25- overridable via `WETH`. The address embeds the wETH argument, so it is64+ and is overridable via `WETH`. Neither contract takes constructor arguments,
26- identical across chains that share one wETH deployment.65+ so both land at the same address on every chain.
27 - `predict()` entrypoint printing the deployment address without broadcasting,66 - `predict()` entrypoint printing the deployment address without broadcasting,
28- and CREATE2 tests pinning address determinism across chain IDs and its67+ and CREATE2 tests pinning address determinism across chain IDs, including
29- dependence on the wETH argument.68+ that two chains with different wETH still give LOVE one address.
30 - `foundry.toml` with pinned solc 0.8.30, optimizer, fmt rules, fuzz/invariant69 - `foundry.toml` with pinned solc 0.8.30, optimizer, fmt rules, fuzz/invariant
31 defaults, and Base Sepolia RPC/Etherscan endpoints driven by env vars.70 defaults, and Base Sepolia RPC/Etherscan endpoints driven by env vars.
32 - `script/multichain-check.sh`, deploying `Love` on two local anvil nodes with71 - `script/multichain-check.sh`, deploying `Love` on two local anvil nodes with
modified README.md +39 -10
@@ -19,13 +19,46 @@ forge fmt # format
1919 anvil # local node
2020 ```
2121
22+## How wETH is chosen
23+
24+LOVE is pegged to wETH, and that wETH is not a constructor argument. It is read
25+at construction time from `WETHRegistry`, which accepts an address only if its
26+`EXTCODEHASH` is one of ten reviewed wETH implementations *and* it survives a
27+one-wei deposit/withdraw round trip. Registration is permissionless, ownerless
28+and write-once.
29+
30+Two things follow. A LOVE instance can never be bound to a typo, a lookalike
31+ERC-20 or an unreviewed wrapper. And because wETH is no longer in the creation
32+code, the creation code is byte-identical everywhere, so CREATE2 gives LOVE one
33+address on every chain instead of one per wETH deployment.
34+
35+The allowlist covers 14 chains across 5 implementations. Deployment is blocked
36+elsewhere, on purpose: the remaining chains either carry bridged ETH with no
37+`deposit()`/`withdraw()`, or put wETH behind an upgradeable proxy, where a
38+codehash commits to a forwarder rather than to behaviour. To re-derive the list
39+from chain state:
40+
41+```shell
42+./script/weth-codehashes.sh
43+```
44+
45+One caveat worth knowing before you trust an instance: the gate proves a
46+candidate *is* a reviewed wETH implementation, not that it is *the* wETH a
47+chain's ecosystem uses. Anyone can deploy a fresh WETH9 copy — same bytecode,
48+no liquidity — and register it first. Nothing is stealable, since it is real
49+WETH9 and fully redeemable, but check `WETH()` against the chain's canonical
50+address before treating a deployment as the real one.
51+
2252 ## Deploy
2353
2454 Deployment uses CREATE2, so the token has the same address on every EVM network.
55+Bringing up a chain is registry, then registration, then token — `run()` does
56+all three and skips whichever has already happened.
2557
2658 ```shell
2759 forge script script/Love.s.sol:LoveScript --sig 'predict()' # print the address
2860
61+WETH=0x4200000000000000000000000000000000000006 \
2962 forge script script/Love.s.sol:LoveScript \
3063 --rpc-url base_sepolia \
3164 --account "$DEPLOYER_ACCOUNT" \
@@ -33,24 +66,20 @@ forge script script/Love.s.sol:LoveScript \
3366 --verify
3467 ```
3568
69+`WETH` defaults to the OP Stack predeploy and is ignored once a wETH is
70+registered. Registration sends 1 wei, which stays in the registry.
71+
3672 To check determinism for real, deploy on two local chains and compare:
3773
3874 ```shell
3975 ./script/multichain-check.sh
4076 ```
4177
42-It starts two anvil nodes (chain IDs 31337 and 31338), broadcasts the script on
43-both, and exits non-zero if the addresses differ. Override `PORT_A`, `PORT_B`,
78+It starts two anvil nodes (chain IDs 31337 and 31338), gives them *different*
79+wETH implementations at *different* addresses, brings both up, and exits
80+non-zero if the two LOVE addresses differ. Override `PORT_A`, `PORT_B`,
4481 `CHAIN_ID_A`, `CHAIN_ID_B` or `SALT` as needed.
4582
46-## Mint
47-
48-```shell
49-cast send <LOVE_ADDRESS> "mint(address,uint256)" <RECIPIENT> 1000000000000000000 \
50- --rpc-url base_sepolia \
51- --account "$DEPLOYER_ACCOUNT"
52-```
53-
5483 ## License
5584
5685 GPL-3.0
@@ -19,13 +19,46 @@ forge fmt # format
19 anvil # local node19 anvil # local node
20 ```20 ```
21 21
22+## How wETH is chosen
23+
24+LOVE is pegged to wETH, and that wETH is not a constructor argument. It is read
25+at construction time from `WETHRegistry`, which accepts an address only if its
26+`EXTCODEHASH` is one of ten reviewed wETH implementations *and* it survives a
27+one-wei deposit/withdraw round trip. Registration is permissionless, ownerless
28+and write-once.
29+
30+Two things follow. A LOVE instance can never be bound to a typo, a lookalike
31+ERC-20 or an unreviewed wrapper. And because wETH is no longer in the creation
32+code, the creation code is byte-identical everywhere, so CREATE2 gives LOVE one
33+address on every chain instead of one per wETH deployment.
34+
35+The allowlist covers 14 chains across 5 implementations. Deployment is blocked
36+elsewhere, on purpose: the remaining chains either carry bridged ETH with no
37+`deposit()`/`withdraw()`, or put wETH behind an upgradeable proxy, where a
38+codehash commits to a forwarder rather than to behaviour. To re-derive the list
39+from chain state:
40+
41+```shell
42+./script/weth-codehashes.sh
43+```
44+
45+One caveat worth knowing before you trust an instance: the gate proves a
46+candidate *is* a reviewed wETH implementation, not that it is *the* wETH a
47+chain's ecosystem uses. Anyone can deploy a fresh WETH9 copy — same bytecode,
48+no liquidity — and register it first. Nothing is stealable, since it is real
49+WETH9 and fully redeemable, but check `WETH()` against the chain's canonical
50+address before treating a deployment as the real one.
51+
22 ## Deploy52 ## Deploy
23 53
24 Deployment uses CREATE2, so the token has the same address on every EVM network.54 Deployment uses CREATE2, so the token has the same address on every EVM network.
55+Bringing up a chain is registry, then registration, then token — `run()` does
56+all three and skips whichever has already happened.
25 57
26 ```shell58 ```shell
27 forge script script/Love.s.sol:LoveScript --sig 'predict()' # print the address59 forge script script/Love.s.sol:LoveScript --sig 'predict()' # print the address
28 60
61+WETH=0x4200000000000000000000000000000000000006 \
29 forge script script/Love.s.sol:LoveScript \62 forge script script/Love.s.sol:LoveScript \
30 --rpc-url base_sepolia \63 --rpc-url base_sepolia \
31 --account "$DEPLOYER_ACCOUNT" \64 --account "$DEPLOYER_ACCOUNT" \
@@ -33,24 +66,20 @@ forge script script/Love.s.sol:LoveScript \
33 --verify66 --verify
34 ```67 ```
35 68
69+`WETH` defaults to the OP Stack predeploy and is ignored once a wETH is
70+registered. Registration sends 1 wei, which stays in the registry.
71+
36 To check determinism for real, deploy on two local chains and compare:72 To check determinism for real, deploy on two local chains and compare:
37 73
38 ```shell74 ```shell
39 ./script/multichain-check.sh75 ./script/multichain-check.sh
40 ```76 ```
41 77
42-It starts two anvil nodes (chain IDs 31337 and 31338), broadcasts the script on78+It starts two anvil nodes (chain IDs 31337 and 31338), gives them *different*
43-both, and exits non-zero if the addresses differ. Override `PORT_A`, `PORT_B`,79+wETH implementations at *different* addresses, brings both up, and exits
80+non-zero if the two LOVE addresses differ. Override `PORT_A`, `PORT_B`,
44 `CHAIN_ID_A`, `CHAIN_ID_B` or `SALT` as needed.81 `CHAIN_ID_A`, `CHAIN_ID_B` or `SALT` as needed.
45 82
46-## Mint
47-
48-```shell
49-cast send <LOVE_ADDRESS> "mint(address,uint256)" <RECIPIENT> 1000000000000000000 \
50- --rpc-url base_sepolia \
51- --account "$DEPLOYER_ACCOUNT"
52-```
53-
54 ## License83 ## License
55 84
56 GPL-3.085 GPL-3.0
modified foundry.toml +4 -1
@@ -11,7 +11,10 @@ optimizer_runs = 200
1111 via_ir = false
1212 bytecode_hash = "none"
1313 verbosity = 2
14-fs_permissions = [{ access = "read", path = "./out" }]
14+fs_permissions = [
15+ { access = "read", path = "./out" },
16+ { access = "read", path = "./test/fixtures" },
17+]
1518
1619 [profile.ci]
1720 fuzz = { runs = 10_000 }
@@ -11,7 +11,10 @@ optimizer_runs = 200
11 via_ir = false11 via_ir = false
12 bytecode_hash = "none"12 bytecode_hash = "none"
13 verbosity = 213 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 @@
11 // SPDX-License-Identifier: MIT
22 pragma solidity ^0.8.30;
33
4+import {IWETH} from "../src/IWETH.sol";
45 import {Love} from "../src/Love.sol";
5-import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
6+import {WETHRegistry} from "../src/WETHRegistry.sol";
67 import {Script, console} from "forge-std/Script.sol";
78
89 /// @title LoveScript
910 /// @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.
1619 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.
1821 bytes32 public constant DEFAULT_SALT = keccak256("LOVE");
1922
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+
2027 /// @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.
2231 address public constant DEFAULT_WETH = 0x4200000000000000000000000000000000000006;
2332
33+ /// @notice The registry, once `run()` has deployed or found it.
34+ WETHRegistry public registry;
35+
2436 /// @notice The token, once `run()` has deployed or found it.
2537 Love public love;
2638
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.
2841 /// @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.
3043 /// @return The deployed token.
3144 function run() public returns (Love) {
32- bytes32 s = salt();
3345 address w = wethAddress();
34- address predicted = predict(s, w);
46+ address predictedRegistry = registryAddress();
47+ address predicted = predict(salt());
3548
3649 console.log("deployer ", CREATE2_FACTORY);
37- console.log("salt ", vm.toString(s));
50+ console.log("registry ", predictedRegistry);
3851 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()));
4054 console.log("predicted ", predicted);
4155
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);
6059
6160 return love;
6261 }
6362
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.
6665 function predict() public view returns (address predicted) {
67- bytes32 s = salt();
68- address w = wethAddress();
69- predicted = predict(s, w);
66+ predicted = predict(salt());
7067
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()));
7472 console.log("predicted ", predicted);
7573 }
7674
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.
7879 /// @param s The CREATE2 salt.
79- /// @param w The wETH the token would be pegged to.
8080 /// @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);
8389 }
8490
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;
9095 }
9196
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());
97101 }
98102
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.
101112 /// @return The configured salt, or `DEFAULT_SALT`.
102113 function salt() public view returns (bytes32) {
103114 return vm.envOr("SALT", DEFAULT_SALT);
104115 }
105116
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.
108120 /// @return The configured wETH address, or `DEFAULT_WETH`.
109121 function wethAddress() public view returns (address) {
110122 return vm.envOr("WETH", DEFAULT_WETH);
111123 }
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+ }
112188 }
@@ -1,112 +1,188 @@
1 // SPDX-License-Identifier: MIT1 // 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 LoveScript9 /// @title LoveScript
9 /// @author Julien Béranger10 /// @author Julien Béranger
10-/// @notice Deploys `Love` with CREATE2 through the canonical deterministic11+/// @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, and12+/// registry first, wETH registered second, token third.
12-/// the creation code embeds the wETH address — so the token only gets13+/// @dev Both contracts go through the canonical deterministic deployer and
13-/// the same address on chains where wETH sits at the same address. Keep14+/// neither takes constructor arguments, so their creation code is
14-/// `solc`, the optimizer settings and `bytecode_hash = "none"` as they15+/// 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 the41 /// @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 script/multichain-check.sh +74 -17
@@ -1,13 +1,22 @@
11 #!/usr/bin/env bash
22 #
3-# Deploys Love on two local anvil nodes with different chain ids and checks
4-# that CREATE2 gives the token the same address on both. The address embeds the
5-# wETH address, so this only holds for chains sharing one wETH deployment —
6-# both nodes here are given the same WETH.
3+# Proves the claim the registry exists to make: LOVE lands at the same address
4+# on two chains that have nothing about their wETH in common.
5+#
6+# Two anvil nodes with different chain ids get different wETH implementations,
7+# at different addresses — node A the OP Stack predeploy, node B canonical
8+# WETH9 at its mainnet address — using the same runtime bytecode the tests
9+# etch. Each node is then brought up the way a real chain would be: registry,
10+# registration, token. If the two token addresses match, wETH is genuinely out
11+# of the creation code.
12+#
13+# The previous version of this script gave both nodes the same wETH, because
14+# back then the address was a constructor argument and matching was only
15+# possible when the argument matched. That is exactly what changed.
716 #
817 # Usage: ./script/multichain-check.sh
918 #
10-# Env overrides: PORT_A, PORT_B, CHAIN_ID_A, CHAIN_ID_B, SALT, WETH
19+# Env overrides: PORT_A, PORT_B, CHAIN_ID_A, CHAIN_ID_B, SALT
1120
1221 set -euo pipefail
1322
@@ -20,7 +29,15 @@ CHAIN_ID_B=${CHAIN_ID_B:-31338}
2029 ANVIL_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
2130 CREATE2_FACTORY=0x4e59b44847b379578588920cA78FbF26c0B4956C
2231
32+# Node A: the OP Stack predeploy. Node B: canonical WETH9, where it sits on
33+# Ethereum. Different implementations, different addresses, both allowlisted.
34+WETH_A=0x4200000000000000000000000000000000000006
35+WETH_B=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
36+
2337 ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
38+FIXTURE_A="$ROOT/test/fixtures/weth9-op-legacy.hex"
39+FIXTURE_B="$ROOT/test/fixtures/weth9-canonical.hex"
40+
2441 RPC_A="http://127.0.0.1:$PORT_A"
2542 RPC_B="http://127.0.0.1:$PORT_B"
2643 TMP=$(mktemp -d)
@@ -53,9 +70,29 @@ start_node() {
5370 exit 1
5471 }
5572
73+# Puts real wETH runtime bytecode at an address, the way the chain itself would
74+# have it. The registry checks EXTCODEHASH, so only genuine bytecode passes —
75+# there is no test-only path through the allowlist.
76+place_weth() {
77+ local rpc=$1 addr=$2 fixture=$3 label=$4
78+
79+ [[ -f $fixture ]] || {
80+ echo "missing fixture $fixture" >&2
81+ exit 1
82+ }
83+
84+ cast rpc anvil_setCode "$addr" "$(tr -d '\n' <"$fixture")" --rpc-url "$rpc" >/dev/null
85+
86+ if [[ $(cast code "$addr" --rpc-url "$rpc") == "0x" ]]; then
87+ echo "$label: failed to place wETH at $addr" >&2
88+ exit 1
89+ fi
90+}
91+
5692 deploy() {
57- local rpc=$1 label=$2
58- forge script script/Love.s.sol:LoveScript \
93+ local rpc=$1 label=$2 weth=$3
94+
95+ WETH=$weth forge script script/Love.s.sol:LoveScript \
5996 --rpc-url "$rpc" \
6097 --private-key "$ANVIL_KEY" \
6198 --broadcast \
@@ -72,6 +109,7 @@ deploy() {
72109 report() {
73110 local rpc=$1 addr=$2 label=$3
74111 local code
112+
75113 code=$(cast code "$addr" --rpc-url "$rpc")
76114
77115 if [[ ${#code} -le 2 ]]; then
@@ -79,11 +117,12 @@ report() {
79117 exit 1
80118 fi
81119
82- printf '%s %s %s bytes %s / %s weth %s\n' \
120+ printf '%s %s %s bytes %s / %s\n' \
83121 "$label" "$addr" "$(((${#code} - 2) / 2))" \
84122 "$(cast call "$addr" 'name()(string)' --rpc-url "$rpc")" \
85- "$(cast call "$addr" 'symbol()(string)' --rpc-url "$rpc")" \
86- "$(cast call "$addr" 'WETH()(address)' --rpc-url "$rpc")"
123+ "$(cast call "$addr" 'symbol()(string)' --rpc-url "$rpc")"
124+ printf '%s registry %s\n' "$label" "$(cast call "$addr" 'REGISTRY()(address)' --rpc-url "$rpc")"
125+ printf '%s weth %s\n' "$label" "$(cast call "$addr" 'WETH()(address)' --rpc-url "$rpc")"
87126 }
88127
89128 cd "$ROOT"
@@ -106,19 +145,37 @@ for pair in "A|$RPC_A" "B|$RPC_B"; do
106145 fi
107146 done
108147
109-forge build >/dev/null
148+forge build >"$TMP/build.log" 2>&1 || {
149+ cat "$TMP/build.log" >&2
150+ exit 1
151+}
152+
153+place_weth "$RPC_A" "$WETH_A" "$FIXTURE_A" A
154+place_weth "$RPC_B" "$WETH_B" "$FIXTURE_B" B
110155
111-ADDR_A=$(deploy "$RPC_A" A)
112-ADDR_B=$(deploy "$RPC_B" B)
156+ADDR_A=$(deploy "$RPC_A" A "$WETH_A")
157+ADDR_B=$(deploy "$RPC_B" B "$WETH_B")
113158
114-echo "chain $CHAIN_ID_A"
159+echo "chain $CHAIN_ID_A — OP Stack legacy WETH9 at $WETH_A"
115160 report "$RPC_A" "$ADDR_A" " A"
116-echo "chain $CHAIN_ID_B"
161+echo "chain $CHAIN_ID_B — canonical WETH9 at $WETH_B"
117162 report "$RPC_B" "$ADDR_B" " B"
163+echo
164+
165+# macOS ships bash 3.2, which has no ${var,,}.
166+lower() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; }
167+
168+WETH_ON_A=$(cast call "$ADDR_A" 'WETH()(address)' --rpc-url "$RPC_A")
169+WETH_ON_B=$(cast call "$ADDR_B" 'WETH()(address)' --rpc-url "$RPC_B")
170+
171+if [[ $(lower "$WETH_ON_A") == "$(lower "$WETH_ON_B")" ]]; then
172+ echo "FAIL: both chains ended up on the same wETH — this proves nothing" >&2
173+ exit 1
174+fi
118175
119-if [[ "$ADDR_A" != "$ADDR_B" ]]; then
176+if [[ $(lower "$ADDR_A") != "$(lower "$ADDR_B")" ]]; then
120177 echo "FAIL: addresses differ" >&2
121178 exit 1
122179 fi
123180
124-echo "OK: same address on both chains"
181+echo "OK: same LOVE address on both chains, backed by different wETH"
@@ -1,13 +1,22 @@
1 #!/usr/bin/env bash1 #!/usr/bin/env bash
2 #2 #
3-# Deploys Love on two local anvil nodes with different chain ids and checks3+# Proves the claim the registry exists to make: LOVE lands at the same address
4-# that CREATE2 gives the token the same address on both. The address embeds the4+# on two chains that have nothing about their wETH in common.
5-# wETH address, so this only holds for chains sharing one wETH deployment —5+#
6-# both nodes here are given the same WETH.6+# Two anvil nodes with different chain ids get different wETH implementations,
7+# at different addresses — node A the OP Stack predeploy, node B canonical
8+# WETH9 at its mainnet address — using the same runtime bytecode the tests
9+# etch. Each node is then brought up the way a real chain would be: registry,
10+# registration, token. If the two token addresses match, wETH is genuinely out
11+# of the creation code.
12+#
13+# The previous version of this script gave both nodes the same wETH, because
14+# back then the address was a constructor argument and matching was only
15+# possible when the argument matched. That is exactly what changed.
7 #16 #
8 # Usage: ./script/multichain-check.sh17 # Usage: ./script/multichain-check.sh
9 #18 #
10-# Env overrides: PORT_A, PORT_B, CHAIN_ID_A, CHAIN_ID_B, SALT, WETH19+# Env overrides: PORT_A, PORT_B, CHAIN_ID_A, CHAIN_ID_B, SALT
11 20
12 set -euo pipefail21 set -euo pipefail
13 22
@@ -20,7 +29,15 @@ CHAIN_ID_B=${CHAIN_ID_B:-31338}
20 ANVIL_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff8029 ANVIL_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
21 CREATE2_FACTORY=0x4e59b44847b379578588920cA78FbF26c0B4956C30 CREATE2_FACTORY=0x4e59b44847b379578588920cA78FbF26c0B4956C
22 31
32+# Node A: the OP Stack predeploy. Node B: canonical WETH9, where it sits on
33+# Ethereum. Different implementations, different addresses, both allowlisted.
34+WETH_A=0x4200000000000000000000000000000000000006
35+WETH_B=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
36+
23 ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)37 ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
38+FIXTURE_A="$ROOT/test/fixtures/weth9-op-legacy.hex"
39+FIXTURE_B="$ROOT/test/fixtures/weth9-canonical.hex"
40+
24 RPC_A="http://127.0.0.1:$PORT_A"41 RPC_A="http://127.0.0.1:$PORT_A"
25 RPC_B="http://127.0.0.1:$PORT_B"42 RPC_B="http://127.0.0.1:$PORT_B"
26 TMP=$(mktemp -d)43 TMP=$(mktemp -d)
@@ -53,9 +70,29 @@ start_node() {
53 exit 170 exit 1
54 }71 }
55 72
73+# Puts real wETH runtime bytecode at an address, the way the chain itself would
74+# have it. The registry checks EXTCODEHASH, so only genuine bytecode passes —
75+# there is no test-only path through the allowlist.
76+place_weth() {
77+ local rpc=$1 addr=$2 fixture=$3 label=$4
78+
79+ [[ -f $fixture ]] || {
80+ echo "missing fixture $fixture" >&2
81+ exit 1
82+ }
83+
84+ cast rpc anvil_setCode "$addr" "$(tr -d '\n' <"$fixture")" --rpc-url "$rpc" >/dev/null
85+
86+ if [[ $(cast code "$addr" --rpc-url "$rpc") == "0x" ]]; then
87+ echo "$label: failed to place wETH at $addr" >&2
88+ exit 1
89+ fi
90+}
91+
56 deploy() {92 deploy() {
57- local rpc=$1 label=$293+ local rpc=$1 label=$2 weth=$3
58- forge script script/Love.s.sol:LoveScript \94+
95+ WETH=$weth forge script script/Love.s.sol:LoveScript \
59 --rpc-url "$rpc" \96 --rpc-url "$rpc" \
60 --private-key "$ANVIL_KEY" \97 --private-key "$ANVIL_KEY" \
61 --broadcast \98 --broadcast \
@@ -72,6 +109,7 @@ deploy() {
72 report() {109 report() {
73 local rpc=$1 addr=$2 label=$3110 local rpc=$1 addr=$2 label=$3
74 local code111 local code
112+
75 code=$(cast code "$addr" --rpc-url "$rpc")113 code=$(cast code "$addr" --rpc-url "$rpc")
76 114
77 if [[ ${#code} -le 2 ]]; then115 if [[ ${#code} -le 2 ]]; then
@@ -79,11 +117,12 @@ report() {
79 exit 1117 exit 1
80 fi118 fi
81 119
82- printf '%s %s %s bytes %s / %s weth %s\n' \120+ printf '%s %s %s bytes %s / %s\n' \
83 "$label" "$addr" "$(((${#code} - 2) / 2))" \121 "$label" "$addr" "$(((${#code} - 2) / 2))" \
84 "$(cast call "$addr" 'name()(string)' --rpc-url "$rpc")" \122 "$(cast call "$addr" 'name()(string)' --rpc-url "$rpc")" \
85- "$(cast call "$addr" 'symbol()(string)' --rpc-url "$rpc")" \123+ "$(cast call "$addr" 'symbol()(string)' --rpc-url "$rpc")"
86- "$(cast call "$addr" 'WETH()(address)' --rpc-url "$rpc")"124+ printf '%s registry %s\n' "$label" "$(cast call "$addr" 'REGISTRY()(address)' --rpc-url "$rpc")"
125+ printf '%s weth %s\n' "$label" "$(cast call "$addr" 'WETH()(address)' --rpc-url "$rpc")"
87 }126 }
88 127
89 cd "$ROOT"128 cd "$ROOT"
@@ -106,19 +145,37 @@ for pair in "A|$RPC_A" "B|$RPC_B"; do
106 fi145 fi
107 done146 done
108 147
109-forge build >/dev/null148+forge build >"$TMP/build.log" 2>&1 || {
149+ cat "$TMP/build.log" >&2
150+ exit 1
151+}
152+
153+place_weth "$RPC_A" "$WETH_A" "$FIXTURE_A" A
154+place_weth "$RPC_B" "$WETH_B" "$FIXTURE_B" B
110 155
111-ADDR_A=$(deploy "$RPC_A" A)156+ADDR_A=$(deploy "$RPC_A" A "$WETH_A")
112-ADDR_B=$(deploy "$RPC_B" B)157+ADDR_B=$(deploy "$RPC_B" B "$WETH_B")
113 158
114-echo "chain $CHAIN_ID_A"159+echo "chain $CHAIN_ID_A — OP Stack legacy WETH9 at $WETH_A"
115 report "$RPC_A" "$ADDR_A" " A"160 report "$RPC_A" "$ADDR_A" " A"
116-echo "chain $CHAIN_ID_B"161+echo "chain $CHAIN_ID_B — canonical WETH9 at $WETH_B"
117 report "$RPC_B" "$ADDR_B" " B"162 report "$RPC_B" "$ADDR_B" " B"
163+echo
164+
165+# macOS ships bash 3.2, which has no ${var,,}.
166+lower() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; }
167+
168+WETH_ON_A=$(cast call "$ADDR_A" 'WETH()(address)' --rpc-url "$RPC_A")
169+WETH_ON_B=$(cast call "$ADDR_B" 'WETH()(address)' --rpc-url "$RPC_B")
170+
171+if [[ $(lower "$WETH_ON_A") == "$(lower "$WETH_ON_B")" ]]; then
172+ echo "FAIL: both chains ended up on the same wETH — this proves nothing" >&2
173+ exit 1
174+fi
118 175
119-if [[ "$ADDR_A" != "$ADDR_B" ]]; then176+if [[ $(lower "$ADDR_A") != "$(lower "$ADDR_B")" ]]; then
120 echo "FAIL: addresses differ" >&2177 echo "FAIL: addresses differ" >&2
121 exit 1178 exit 1
122 fi179 fi
123 180
124-echo "OK: same address on both chains"181+echo "OK: same LOVE address on both chains, backed by different wETH"
added script/weth-chains.tsv +36 -0
new file mode 100644
@@ -0,0 +1,36 @@
1+# The wETH deployment on each chain, and a public RPC to read it from.
2+#
3+# Input to ./weth-codehashes.sh, which hashes the code at each address to find
4+# out how many distinct wETH implementations are actually in use. Entries are
5+# claims, not facts: the script calls name/symbol/decimals on each one and
6+# flags anything that does not answer like wETH, so a wrong address here shows
7+# up as a bad row rather than as a bogus hash in the allowlist.
8+#
9+# chain_id name weth rpc
10+1 ethereum 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 https://ethereum-rpc.publicnode.com
11+10 optimism 0x4200000000000000000000000000000000000006 https://optimism-rpc.publicnode.com
12+56 bnb-chain 0x2170Ed0880ac9A755fd29B2688956BD959F933F8 https://bsc-rpc.publicnode.com
13+100 gnosis 0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1 https://gnosis-rpc.publicnode.com
14+130 unichain 0x4200000000000000000000000000000000000006 https://unichain-rpc.publicnode.com
15+137 polygon 0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619 https://polygon-bor-rpc.publicnode.com
16+204 opbnb 0xE7798f023fC62146e8Aa1b36Da45fb70855a77Ea https://opbnb-rpc.publicnode.com
17+252 fraxtal 0xFC00000000000000000000000000000000000006 https://fraxtal.drpc.org
18+324 zksync-era 0x5AEa5775959fBC2557Cc8789bC1bf90A239D9a91 https://mainnet.era.zksync.io
19+480 world-chain 0x4200000000000000000000000000000000000006 https://worldchain-mainnet.g.alchemy.com/public
20+1088 metis 0x420000000000000000000000000000000000000A https://andromeda.metis.io/?owner=1088
21+1101 polygon-zkevm 0x4F9A0e7FD2Bf6067db6994CF12E4495Df938E6e9 https://polygon-zkevm.drpc.org
22+1135 lisk 0x4200000000000000000000000000000000000006 https://rpc.api.lisk.com
23+1868 soneium 0x4200000000000000000000000000000000000006 https://rpc.soneium.org
24+5000 mantle 0xdEAddEaDdeadDEadDEADDEAdDEADDEaDDEAD1111 https://rpc.mantle.xyz
25+8453 base 0x4200000000000000000000000000000000000006 https://base-rpc.publicnode.com
26+34443 mode 0x4200000000000000000000000000000000000006 https://mode.drpc.org
27+42161 arbitrum-one 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1 https://arbitrum-one-rpc.publicnode.com
28+42220 celo 0xD221812de1BD094f35587EE8E174B07B6167D9Af https://forno.celo.org
29+43114 avalanche 0x49D5c2BdFfac6CE2BFdB6640F4F80f226bc10bAB https://avalanche-c-chain-rpc.publicnode.com
30+57073 ink 0x4200000000000000000000000000000000000006 https://ink.drpc.org
31+59144 linea 0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f https://rpc.linea.build
32+80094 berachain 0x2F6F07CDcf3588944Bf4C42aC74ff24bF56e7590 https://rpc.berachain.com
33+81457 blast 0x4300000000000000000000000000000000000004 https://blast.drpc.org
34+167000 taiko 0xA51894664A773981C6C112C43ce576f315d5b1B6 https://rpc.mainnet.taiko.xyz
35+534352 scroll 0x5300000000000000000000000000000000000004 https://scroll.drpc.org
36+7777777 zora 0x4200000000000000000000000000000000000006 https://rpc.zora.energy
new file mode 100644
@@ -0,0 +1,36 @@
1+# The wETH deployment on each chain, and a public RPC to read it from.
2+#
3+# Input to ./weth-codehashes.sh, which hashes the code at each address to find
4+# out how many distinct wETH implementations are actually in use. Entries are
5+# claims, not facts: the script calls name/symbol/decimals on each one and
6+# flags anything that does not answer like wETH, so a wrong address here shows
7+# up as a bad row rather than as a bogus hash in the allowlist.
8+#
9+# chain_id name weth rpc
10+1 ethereum 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 https://ethereum-rpc.publicnode.com
11+10 optimism 0x4200000000000000000000000000000000000006 https://optimism-rpc.publicnode.com
12+56 bnb-chain 0x2170Ed0880ac9A755fd29B2688956BD959F933F8 https://bsc-rpc.publicnode.com
13+100 gnosis 0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1 https://gnosis-rpc.publicnode.com
14+130 unichain 0x4200000000000000000000000000000000000006 https://unichain-rpc.publicnode.com
15+137 polygon 0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619 https://polygon-bor-rpc.publicnode.com
16+204 opbnb 0xE7798f023fC62146e8Aa1b36Da45fb70855a77Ea https://opbnb-rpc.publicnode.com
17+252 fraxtal 0xFC00000000000000000000000000000000000006 https://fraxtal.drpc.org
18+324 zksync-era 0x5AEa5775959fBC2557Cc8789bC1bf90A239D9a91 https://mainnet.era.zksync.io
19+480 world-chain 0x4200000000000000000000000000000000000006 https://worldchain-mainnet.g.alchemy.com/public
20+1088 metis 0x420000000000000000000000000000000000000A https://andromeda.metis.io/?owner=1088
21+1101 polygon-zkevm 0x4F9A0e7FD2Bf6067db6994CF12E4495Df938E6e9 https://polygon-zkevm.drpc.org
22+1135 lisk 0x4200000000000000000000000000000000000006 https://rpc.api.lisk.com
23+1868 soneium 0x4200000000000000000000000000000000000006 https://rpc.soneium.org
24+5000 mantle 0xdEAddEaDdeadDEadDEADDEAdDEADDEaDDEAD1111 https://rpc.mantle.xyz
25+8453 base 0x4200000000000000000000000000000000000006 https://base-rpc.publicnode.com
26+34443 mode 0x4200000000000000000000000000000000000006 https://mode.drpc.org
27+42161 arbitrum-one 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1 https://arbitrum-one-rpc.publicnode.com
28+42220 celo 0xD221812de1BD094f35587EE8E174B07B6167D9Af https://forno.celo.org
29+43114 avalanche 0x49D5c2BdFfac6CE2BFdB6640F4F80f226bc10bAB https://avalanche-c-chain-rpc.publicnode.com
30+57073 ink 0x4200000000000000000000000000000000000006 https://ink.drpc.org
31+59144 linea 0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f https://rpc.linea.build
32+80094 berachain 0x2F6F07CDcf3588944Bf4C42aC74ff24bF56e7590 https://rpc.berachain.com
33+81457 blast 0x4300000000000000000000000000000000000004 https://blast.drpc.org
34+167000 taiko 0xA51894664A773981C6C112C43ce576f315d5b1B6 https://rpc.mainnet.taiko.xyz
35+534352 scroll 0x5300000000000000000000000000000000000004 https://scroll.drpc.org
36+7777777 zora 0x4200000000000000000000000000000000000006 https://rpc.zora.energy
added script/weth-codehashes.sh +163 -0
new file mode 100755
@@ -0,0 +1,163 @@
1+#!/usr/bin/env bash
2+#
3+# Surveys the wETH deployment on every chain in weth-chains.tsv and groups them
4+# by implementation, to answer the question the allowlist in
5+# src/WETHRegistry.sol depends on: how many distinct wETH implementations are
6+# actually out there?
7+#
8+# For each chain it reports three things:
9+#
10+# codehash keccak256 of the runtime code — exactly what EXTCODEHASH returns
11+# and what the registry compares against, so these values are the
12+# allowlist entries verbatim.
13+# family the same hash with solc's trailing metadata blob removed. That
14+# blob is never executed and embeds a hash of the source path and
15+# comments, so one implementation compiled twice gets two
16+# codehashes but one family. Grouping by family is how the exact
17+# hashes get reviewed together; the registry itself never strips.
18+# kind whether the code carries the ERC-20 and wrapper entrypoints
19+# itself. A bridged-ETH token has no deposit/withdraw, and a proxy
20+# has no entrypoints at all — its codehash commits to a forwarder,
21+# not to behaviour, so it must never be allowlisted.
22+#
23+# Chains are read with eth_getCode and hashed locally rather than with
24+# eth_getProof, which several public RPCs do not serve.
25+#
26+# Usage: ./script/weth-codehashes.sh [chains.tsv]
27+#
28+# Exits non-zero if any row is unreachable, so a partial survey cannot be
29+# mistaken for a clean one.
30+
31+set -uo pipefail
32+
33+ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
34+CHAINS=${1:-$ROOT/script/weth-chains.tsv}
35+
36+for cmd in cast; do
37+ command -v "$cmd" >/dev/null || {
38+ echo "$cmd not found — install foundry" >&2
39+ exit 1
40+ }
41+done
42+
43+[[ -f $CHAINS ]] || {
44+ echo "no chain table at $CHAINS" >&2
45+ exit 1
46+}
47+
48+# ERC-20 plus the two wrapper entrypoints. Code missing any of these is not a
49+# wETH this project can peg to, whatever it is called.
50+SELECTORS=(
51+ d0e30db0 # deposit()
52+ 2e1a7d4d # withdraw(uint256)
53+ a9059cbb # transfer(address,uint256)
54+ 70a08231 # balanceOf(address)
55+ 095ea7b3 # approve(address,uint256)
56+ 23b872dd # transferFrom(address,address,uint256)
57+)
58+
59+# Drops solc's metadata blob: the last two bytes hold its length, and the blob
60+# itself starts with a CBOR map (0xa1, 0xa2, …). Anything that fails those two
61+# sanity checks is left whole rather than guessed at.
62+strip_metadata() {
63+ local hex=$1 len total marker
64+ total=$((${#hex} / 2))
65+ [[ ${#hex} -gt 4 ]] || {
66+ printf '%s' "$hex"
67+ return
68+ }
69+
70+ len=$((16#${hex: -4}))
71+ if ((len + 2 >= total || len == 0)); then
72+ printf '%s' "$hex"
73+ return
74+ fi
75+
76+ marker=${hex:$((${#hex} - (len + 2) * 2)):2}
77+ if [[ $marker != a* ]]; then
78+ printf '%s' "$hex"
79+ return
80+ fi
81+
82+ printf '%s' "${hex:0:$((${#hex} - (len + 2) * 2))}"
83+}
84+
85+rows=()
86+unreachable=0
87+
88+printf '%-16s %-9s %-8s %-12s %s\n' CHAIN ID BYTES KIND CODEHASH
89+
90+while IFS=$'\t' read -r chain_id name weth rpc; do
91+ [[ -z ${chain_id:-} || $chain_id == \#* ]] && continue
92+
93+ code=$(cast code "$weth" --rpc-url "$rpc" 2>/dev/null)
94+
95+ if [[ ! $code == 0x* || ${#code} -le 4 ]]; then
96+ printf '%-16s %-9s %-8s %-12s %s\n' "$name" "$chain_id" "-" "-" "UNREACHABLE or no code"
97+ unreachable=$((unreachable + 1))
98+ continue
99+ fi
100+
101+ hex=${code#0x}
102+ body=$(strip_metadata "$hex")
103+
104+ codehash=$(cast keccak "0x$hex")
105+ family=$(cast keccak "0x$body")
106+
107+ kind=wrapper
108+ for selector in "${SELECTORS[@]}"; do
109+ [[ $body == *"$selector"* ]] || {
110+ kind=not-wETH
111+ break
112+ }
113+ done
114+
115+ printf '%-16s %-9s %-8s %-12s %s\n' "$name" "$chain_id" "$((${#hex} / 2))" "$kind" "$codehash"
116+ rows+=("$family|$codehash|$name|$kind|$((${#hex} / 2))")
117+done <"$CHAINS"
118+
119+echo
120+echo "families — one block per implementation, indented lines are the allowlist entries"
121+echo
122+
123+wrapper_chains=0
124+wrapper_hashes=0
125+wrapper_families=0
126+
127+for family in $(printf '%s\n' ${rows[@]+"${rows[@]}"} | cut -d'|' -f1 | sort -u); do
128+ block=$(printf '%s\n' ${rows[@]+"${rows[@]}"} | grep "^$family|")
129+ kinds=$(printf '%s\n' "$block" | cut -d'|' -f4 | sort -u | paste -sd, -)
130+ size=$(printf '%s\n' "$block" | head -1 | cut -d'|' -f5)
131+ chains=$(printf '%s\n' "$block" | wc -l | tr -d ' ')
132+
133+ printf '%s %s bytes %s chain(s) %s\n' "${family:0:18}" "$size" "$chains" "$kinds"
134+
135+ for codehash in $(printf '%s\n' "$block" | cut -d'|' -f2 | sort -u); do
136+ names=$(printf '%s\n' "$block" | grep "|$codehash|" | cut -d'|' -f3 | sort | paste -sd' ' -)
137+ printf ' %s %s\n' "$codehash" "$names"
138+
139+ if [[ $kinds == wrapper ]]; then
140+ wrapper_hashes=$((wrapper_hashes + 1))
141+ fi
142+ done
143+
144+ if [[ $kinds == wrapper ]]; then
145+ wrapper_families=$((wrapper_families + 1))
146+ wrapper_chains=$((wrapper_chains + chains))
147+ fi
148+
149+ echo
150+done
151+
152+printf '%s chains read, %s unreachable\n' "${#rows[@]}" "$unreachable"
153+printf 'wrappers: %s chains, %s families, %s exact codehashes to allowlist\n' \
154+ "$wrapper_chains" "$wrapper_families" "$wrapper_hashes"
155+echo
156+echo "A wrapper family is a candidate, not an entry — read its source before"
157+echo "adding the hash. Proxies and bridged-ETH tokens show up as not-wETH."
158+
159+[[ $unreachable -eq 0 ]] || {
160+ echo >&2
161+ echo "$unreachable row(s) unreachable — survey is incomplete" >&2
162+ exit 1
163+}
new file mode 100755
@@ -0,0 +1,163 @@
1+#!/usr/bin/env bash
2+#
3+# Surveys the wETH deployment on every chain in weth-chains.tsv and groups them
4+# by implementation, to answer the question the allowlist in
5+# src/WETHRegistry.sol depends on: how many distinct wETH implementations are
6+# actually out there?
7+#
8+# For each chain it reports three things:
9+#
10+# codehash keccak256 of the runtime code — exactly what EXTCODEHASH returns
11+# and what the registry compares against, so these values are the
12+# allowlist entries verbatim.
13+# family the same hash with solc's trailing metadata blob removed. That
14+# blob is never executed and embeds a hash of the source path and
15+# comments, so one implementation compiled twice gets two
16+# codehashes but one family. Grouping by family is how the exact
17+# hashes get reviewed together; the registry itself never strips.
18+# kind whether the code carries the ERC-20 and wrapper entrypoints
19+# itself. A bridged-ETH token has no deposit/withdraw, and a proxy
20+# has no entrypoints at all — its codehash commits to a forwarder,
21+# not to behaviour, so it must never be allowlisted.
22+#
23+# Chains are read with eth_getCode and hashed locally rather than with
24+# eth_getProof, which several public RPCs do not serve.
25+#
26+# Usage: ./script/weth-codehashes.sh [chains.tsv]
27+#
28+# Exits non-zero if any row is unreachable, so a partial survey cannot be
29+# mistaken for a clean one.
30+
31+set -uo pipefail
32+
33+ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
34+CHAINS=${1:-$ROOT/script/weth-chains.tsv}
35+
36+for cmd in cast; do
37+ command -v "$cmd" >/dev/null || {
38+ echo "$cmd not found — install foundry" >&2
39+ exit 1
40+ }
41+done
42+
43+[[ -f $CHAINS ]] || {
44+ echo "no chain table at $CHAINS" >&2
45+ exit 1
46+}
47+
48+# ERC-20 plus the two wrapper entrypoints. Code missing any of these is not a
49+# wETH this project can peg to, whatever it is called.
50+SELECTORS=(
51+ d0e30db0 # deposit()
52+ 2e1a7d4d # withdraw(uint256)
53+ a9059cbb # transfer(address,uint256)
54+ 70a08231 # balanceOf(address)
55+ 095ea7b3 # approve(address,uint256)
56+ 23b872dd # transferFrom(address,address,uint256)
57+)
58+
59+# Drops solc's metadata blob: the last two bytes hold its length, and the blob
60+# itself starts with a CBOR map (0xa1, 0xa2, …). Anything that fails those two
61+# sanity checks is left whole rather than guessed at.
62+strip_metadata() {
63+ local hex=$1 len total marker
64+ total=$((${#hex} / 2))
65+ [[ ${#hex} -gt 4 ]] || {
66+ printf '%s' "$hex"
67+ return
68+ }
69+
70+ len=$((16#${hex: -4}))
71+ if ((len + 2 >= total || len == 0)); then
72+ printf '%s' "$hex"
73+ return
74+ fi
75+
76+ marker=${hex:$((${#hex} - (len + 2) * 2)):2}
77+ if [[ $marker != a* ]]; then
78+ printf '%s' "$hex"
79+ return
80+ fi
81+
82+ printf '%s' "${hex:0:$((${#hex} - (len + 2) * 2))}"
83+}
84+
85+rows=()
86+unreachable=0
87+
88+printf '%-16s %-9s %-8s %-12s %s\n' CHAIN ID BYTES KIND CODEHASH
89+
90+while IFS=$'\t' read -r chain_id name weth rpc; do
91+ [[ -z ${chain_id:-} || $chain_id == \#* ]] && continue
92+
93+ code=$(cast code "$weth" --rpc-url "$rpc" 2>/dev/null)
94+
95+ if [[ ! $code == 0x* || ${#code} -le 4 ]]; then
96+ printf '%-16s %-9s %-8s %-12s %s\n' "$name" "$chain_id" "-" "-" "UNREACHABLE or no code"
97+ unreachable=$((unreachable + 1))
98+ continue
99+ fi
100+
101+ hex=${code#0x}
102+ body=$(strip_metadata "$hex")
103+
104+ codehash=$(cast keccak "0x$hex")
105+ family=$(cast keccak "0x$body")
106+
107+ kind=wrapper
108+ for selector in "${SELECTORS[@]}"; do
109+ [[ $body == *"$selector"* ]] || {
110+ kind=not-wETH
111+ break
112+ }
113+ done
114+
115+ printf '%-16s %-9s %-8s %-12s %s\n' "$name" "$chain_id" "$((${#hex} / 2))" "$kind" "$codehash"
116+ rows+=("$family|$codehash|$name|$kind|$((${#hex} / 2))")
117+done <"$CHAINS"
118+
119+echo
120+echo "families — one block per implementation, indented lines are the allowlist entries"
121+echo
122+
123+wrapper_chains=0
124+wrapper_hashes=0
125+wrapper_families=0
126+
127+for family in $(printf '%s\n' ${rows[@]+"${rows[@]}"} | cut -d'|' -f1 | sort -u); do
128+ block=$(printf '%s\n' ${rows[@]+"${rows[@]}"} | grep "^$family|")
129+ kinds=$(printf '%s\n' "$block" | cut -d'|' -f4 | sort -u | paste -sd, -)
130+ size=$(printf '%s\n' "$block" | head -1 | cut -d'|' -f5)
131+ chains=$(printf '%s\n' "$block" | wc -l | tr -d ' ')
132+
133+ printf '%s %s bytes %s chain(s) %s\n' "${family:0:18}" "$size" "$chains" "$kinds"
134+
135+ for codehash in $(printf '%s\n' "$block" | cut -d'|' -f2 | sort -u); do
136+ names=$(printf '%s\n' "$block" | grep "|$codehash|" | cut -d'|' -f3 | sort | paste -sd' ' -)
137+ printf ' %s %s\n' "$codehash" "$names"
138+
139+ if [[ $kinds == wrapper ]]; then
140+ wrapper_hashes=$((wrapper_hashes + 1))
141+ fi
142+ done
143+
144+ if [[ $kinds == wrapper ]]; then
145+ wrapper_families=$((wrapper_families + 1))
146+ wrapper_chains=$((wrapper_chains + chains))
147+ fi
148+
149+ echo
150+done
151+
152+printf '%s chains read, %s unreachable\n' "${#rows[@]}" "$unreachable"
153+printf 'wrappers: %s chains, %s families, %s exact codehashes to allowlist\n' \
154+ "$wrapper_chains" "$wrapper_families" "$wrapper_hashes"
155+echo
156+echo "A wrapper family is a candidate, not an entry — read its source before"
157+echo "adding the hash. Proxies and bridged-ETH tokens show up as not-wETH."
158+
159+[[ $unreachable -eq 0 ]] || {
160+ echo >&2
161+ echo "$unreachable row(s) unreachable — survey is incomplete" >&2
162+ exit 1
163+}
added src/IWETH.sol +20 -0
new file mode 100644
@@ -0,0 +1,20 @@
1+// SPDX-License-Identifier: MIT
2+pragma solidity ^0.8.30;
3+
4+import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
6+/// @title IWETH
7+/// @author Julien Béranger
8+/// @notice The wrapped-ether surface `WETHRegistry` probes: ERC-20, plus the
9+/// two entrypoints that make a wrapper a wrapper.
10+/// @dev Deliberately not the full WETH9 ABI. The registry only needs to move
11+/// one wei in and back out again, and a narrower interface is a narrower
12+/// set of assumptions about what a candidate has to implement.
13+interface IWETH is IERC20 {
14+ /// @notice Wrap the ether sent with this call, one for one.
15+ function deposit() external payable;
16+
17+ /// @notice Unwrap `amount`, burning the wrapper token and returning ether.
18+ /// @param amount The wrapped ether to unwrap, in wei.
19+ function withdraw(uint256 amount) external;
20+}
new file mode 100644
@@ -0,0 +1,20 @@
1+// SPDX-License-Identifier: MIT
2+pragma solidity ^0.8.30;
3+
4+import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
6+/// @title IWETH
7+/// @author Julien Béranger
8+/// @notice The wrapped-ether surface `WETHRegistry` probes: ERC-20, plus the
9+/// two entrypoints that make a wrapper a wrapper.
10+/// @dev Deliberately not the full WETH9 ABI. The registry only needs to move
11+/// one wei in and back out again, and a narrower interface is a narrower
12+/// set of assumptions about what a candidate has to implement.
13+interface IWETH is IERC20 {
14+ /// @notice Wrap the ether sent with this call, one for one.
15+ function deposit() external payable;
16+
17+ /// @notice Unwrap `amount`, burning the wrapper token and returning ether.
18+ /// @param amount The wrapped ether to unwrap, in wei.
19+ function withdraw(uint256 amount) external;
20+}
modified src/Love.sol +42 -5
@@ -1,6 +1,8 @@
11 // SPDX-License-Identifier: MIT
22 pragma solidity ^0.8.30;
33
4+import {IWETH} from "./IWETH.sol";
5+import {WETHRegistry} from "./WETHRegistry.sol";
46 import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
57 import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
68 import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
@@ -21,9 +23,23 @@ contract Love is ERC20 {
2123 /// @notice LOVE minted per unit of wETH, and burned per unit released.
2224 uint256 public constant RATE = 100_000;
2325
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+
2440 /// @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.
2743 IERC20 public immutable WETH;
2844
2945 /// @notice Thrown when a withdrawal amount is not a multiple of `RATE`.
@@ -31,6 +47,14 @@ contract Love is ERC20 {
3147 /// @param rate The rate it has to be a multiple of.
3248 error AmountNotDivisibleByRate(uint256 loveAmount, uint256 rate);
3349
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+
3458 /// @notice Emitted when wETH is locked and LOVE minted.
3559 /// @param account The depositor, who pays the wETH and receives the LOVE.
3660 /// @param wethAmount The wETH pulled in.
@@ -43,9 +67,22 @@ contract Love is ERC20 {
4367 /// @param wethAmount The wETH released, `loveAmount / RATE`.
4468 event Withdraw(address indexed account, uint256 loveAmount, uint256 wethAmount);
4569
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));
4986 }
5087
5188 /// @notice Lock `wethAmount` wETH and mint `wethAmount * RATE` LOVE to the caller.
@@ -1,6 +1,8 @@
1 // SPDX-License-Identifier: MIT1 // 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 this41+ /// @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.
added src/WETHRegistry.sol +203 -0
new file mode 100644
@@ -0,0 +1,203 @@
1+// SPDX-License-Identifier: MIT
2+pragma solidity ^0.8.30;
3+
4+import {IWETH} from "./IWETH.sol";
5+
6+/// @title WETHRegistry
7+/// @author Julien Béranger
8+/// @notice Names the wETH that `Love` pegs to on this chain, and will only
9+/// accept an address whose code is one of a fixed set of reviewed wETH
10+/// implementations.
11+/// @dev The point is to take the wETH address out of `Love`'s creation code.
12+/// Anyone can call `register`, on any chain, without a factory, an owner
13+/// or a per-chain deployer — the contract decides for itself whether to
14+/// accept the candidate. Because it takes no constructor arguments, its
15+/// own creation code is identical everywhere, so CREATE2 puts it at one
16+/// address on every chain and `Love` can hardcode that address.
17+///
18+/// Two gates run at registration:
19+///
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.
26+/// 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 the
28+/// wrapper actually works on this chain.
29+///
30+/// 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 can
32+/// never have the token under its peg swapped.
33+///
34+/// Known limitation, and it is not a small one: the allowlist proves a
35+/// candidate *is* a reviewed wETH implementation, not that it is *the*
36+/// wETH the chain's ecosystem uses. Anyone can deploy their own copy of
37+/// WETH9 — identical code, identical codehash, no liquidity — and
38+/// register it first. The result is still fully backed and redeemable,
39+/// since it is real WETH9, but it is not the token anyone else holds, and
40+/// write-once means the mistake cannot be corrected on that chain. So
41+/// always check `weth()` against the chain's canonical wETH before
42+/// treating a `Love` instance as the real one; a squatted registry is
43+/// visible to anyone who looks, and the answer to it is a fresh salt.
44+contract WETHRegistry {
45+ /// @notice The wETH registered on this chain, or the zero address if none
46+ /// has been registered yet.
47+ /// @dev Written exactly once, by whoever calls `register` first with a
48+ /// candidate that passes both gates.
49+ IWETH public weth;
50+
51+ /// @notice The ether moved through the candidate to prove it wraps.
52+ /// @dev Stays in the registry afterwards; see `register`.
53+ uint256 public constant PROBE = 1 wei;
54+
55+ /// @dev Open only for the duration of the round trip, so the registry
56+ /// cannot be used as a place to park ether. Transient, so it costs
57+ /// almost nothing and cannot survive the call that set it.
58+ bool private transient _probing;
59+
60+ /// @notice Thrown when a wETH has already been registered on this chain.
61+ /// @param registered The wETH registered by the earlier call.
62+ error AlreadyRegistered(IWETH registered);
63+
64+ /// @notice Thrown when the candidate's code is not a reviewed wETH.
65+ /// @param candidate The rejected address.
66+ /// @param codeHash Its `EXTCODEHASH`, zero if there is no code there.
67+ error UnknownImplementation(IWETH candidate, bytes32 codeHash);
68+
69+ /// @notice Thrown when the call does not carry exactly `PROBE` wei.
70+ /// @param sent The value that came with the call.
71+ error ProbeValueRequired(uint256 sent);
72+
73+ /// @notice Thrown when wrapping `PROBE` wei did not mint `PROBE` wrapped.
74+ /// @param expected The balance a real wrapper would have produced.
75+ /// @param actual The balance the candidate produced.
76+ error DepositMismatch(uint256 expected, uint256 actual);
77+
78+ /// @notice Thrown when unwrapping did not burn the wrapped token.
79+ /// @param expected The balance a real wrapper would have left behind.
80+ /// @param actual The balance the candidate left behind.
81+ error WithdrawMismatch(uint256 expected, uint256 actual);
82+
83+ /// @notice Thrown when unwrapping did not return the ether.
84+ /// @param expected The ether balance the round trip should have restored.
85+ /// @param actual The ether balance it actually left.
86+ error EtherNotReturned(uint256 expected, uint256 actual);
87+
88+ /// @notice Thrown when ether is sent outside a round trip.
89+ error NotProbing();
90+
91+ /// @notice Emitted once, when a chain's wETH is settled.
92+ /// @param weth The accepted wETH.
93+ /// @param registrar Whoever supplied and paid for it.
94+ /// @param codeHash The allowlisted hash its code matched.
95+ event Registered(IWETH indexed weth, address indexed registrar, bytes32 codeHash);
96+
97+ /// @notice Accept `candidate` as this chain's wETH, if its code is one of
98+ /// the reviewed implementations and it wraps ether correctly.
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+ ///
106+ /// Reverts rather than degrading when the candidate is unrecognised —
107+ /// unreviewed bytecode cannot be shown safe by any test, static or
108+ /// behavioural, so a chain running its own wETH is a chain `Love`
109+ /// does not deploy on until that implementation is reviewed and
110+ /// added.
111+ /// @param candidate The wETH to register.
112+ /// @return The registered wETH, for the convenience of scripts.
113+ function register(IWETH candidate) external payable returns (IWETH) {
114+ if (address(weth) != address(0)) revert AlreadyRegistered(weth);
115+ if (msg.value != PROBE) revert ProbeValueRequired(msg.value);
116+
117+ bytes32 codeHash = address(candidate).codehash;
118+ if (!isKnownImplementation(codeHash)) revert UnknownImplementation(candidate, codeHash);
119+
120+ _probe(candidate);
121+
122+ weth = candidate;
123+ emit Registered(candidate, msg.sender, codeHash);
124+
125+ return candidate;
126+ }
127+
128+ /// @notice Whether `codeHash` is one of the reviewed wETH implementations.
129+ /// @dev Compile-time, so the list is ownerless and append-only by
130+ /// construction: extending it means publishing a new registry, which
131+ /// leaves every existing deployment exactly as it was. No key can
132+ /// revoke an entry and strand a live `Love`.
133+ ///
134+ /// These are exact `EXTCODEHASH` values, metadata included. Hashing
135+ /// the code with solc's trailing metadata stripped would fold each
136+ /// family into a single entry, but it would also accept a known
137+ /// implementation followed by arbitrary appended bytes, and the
138+ /// safety of that rests on control flow never reaching them — a
139+ /// property that holds for every family here and would have to keep
140+ /// holding for every family added later. Ten constants is the cheaper
141+ /// side of that trade.
142+ ///
143+ /// Grouped by implementation. Every value is reproducible from chain
144+ /// state with `script/weth-codehashes.sh`.
145+ /// @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) {
148+ // OP Stack legacy WETH9, 2041 bytes, solc 0.5.17. One implementation,
149+ // four hashes: these chains run byte-identical code and differ only
150+ // 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
160+
161+ // OP Stack WETH, 2865 bytes, solc 0.8.15.
162+ if (codeHash == 0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012) return true; // unichain, soneium, ink
163+
164+ // Taiko, 3204 bytes.
165+ if (codeHash == 0x9f3d95086909fce850d997158aba31abe26c3aad6a413107ca0bf9d53a7c42e9) return true; // taiko
166+
167+ // Scroll, 5871 bytes.
168+ if (codeHash == 0xe8c4073351c26b9831c1e5af153b9be4713a4af9edfdf32b58077b735e120f14) return true; // scroll
169+
170+ return false;
171+ }
172+
173+ /// @notice Take `PROBE` wei through the candidate and back.
174+ /// @dev Costs the registrar nothing but gas: the wei returns. Balances are
175+ /// read before and after rather than assumed to start at zero, so a
176+ /// candidate that was sent wETH beforehand cannot skew the check.
177+ /// @param candidate The wETH being probed.
178+ function _probe(IWETH candidate) private {
179+ uint256 etherBefore = address(this).balance;
180+ uint256 wrappedBefore = candidate.balanceOf(address(this));
181+
182+ _probing = true;
183+
184+ candidate.deposit{value: PROBE}();
185+ uint256 wrapped = candidate.balanceOf(address(this));
186+ if (wrapped != wrappedBefore + PROBE) revert DepositMismatch(wrappedBefore + PROBE, wrapped);
187+
188+ candidate.withdraw(PROBE);
189+ uint256 unwrapped = candidate.balanceOf(address(this));
190+ if (unwrapped != wrappedBefore) revert WithdrawMismatch(wrappedBefore, unwrapped);
191+
192+ _probing = false;
193+
194+ if (address(this).balance != etherBefore) revert EtherNotReturned(etherBefore, address(this).balance);
195+ }
196+
197+ /// @notice Takes the ether a candidate returns mid-probe, and nothing else.
198+ /// @dev The registry is not a wallet. Outside a round trip this reverts,
199+ /// so ether cannot be stranded in a contract with no way to move it.
200+ receive() external payable {
201+ if (!_probing) revert NotProbing();
202+ }
203+}
new file mode 100644
@@ -0,0 +1,203 @@
1+// SPDX-License-Identifier: MIT
2+pragma solidity ^0.8.30;
3+
4+import {IWETH} from "./IWETH.sol";
5+
6+/// @title WETHRegistry
7+/// @author Julien Béranger
8+/// @notice Names the wETH that `Love` pegs to on this chain, and will only
9+/// accept an address whose code is one of a fixed set of reviewed wETH
10+/// implementations.
11+/// @dev The point is to take the wETH address out of `Love`'s creation code.
12+/// Anyone can call `register`, on any chain, without a factory, an owner
13+/// or a per-chain deployer — the contract decides for itself whether to
14+/// accept the candidate. Because it takes no constructor arguments, its
15+/// own creation code is identical everywhere, so CREATE2 puts it at one
16+/// address on every chain and `Love` can hardcode that address.
17+///
18+/// Two gates run at registration:
19+///
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.
26+/// 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 the
28+/// wrapper actually works on this chain.
29+///
30+/// 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 can
32+/// never have the token under its peg swapped.
33+///
34+/// Known limitation, and it is not a small one: the allowlist proves a
35+/// candidate *is* a reviewed wETH implementation, not that it is *the*
36+/// wETH the chain's ecosystem uses. Anyone can deploy their own copy of
37+/// WETH9 — identical code, identical codehash, no liquidity — and
38+/// register it first. The result is still fully backed and redeemable,
39+/// since it is real WETH9, but it is not the token anyone else holds, and
40+/// write-once means the mistake cannot be corrected on that chain. So
41+/// always check `weth()` against the chain's canonical wETH before
42+/// treating a `Love` instance as the real one; a squatted registry is
43+/// visible to anyone who looks, and the answer to it is a fresh salt.
44+contract WETHRegistry {
45+ /// @notice The wETH registered on this chain, or the zero address if none
46+ /// has been registered yet.
47+ /// @dev Written exactly once, by whoever calls `register` first with a
48+ /// candidate that passes both gates.
49+ IWETH public weth;
50+
51+ /// @notice The ether moved through the candidate to prove it wraps.
52+ /// @dev Stays in the registry afterwards; see `register`.
53+ uint256 public constant PROBE = 1 wei;
54+
55+ /// @dev Open only for the duration of the round trip, so the registry
56+ /// cannot be used as a place to park ether. Transient, so it costs
57+ /// almost nothing and cannot survive the call that set it.
58+ bool private transient _probing;
59+
60+ /// @notice Thrown when a wETH has already been registered on this chain.
61+ /// @param registered The wETH registered by the earlier call.
62+ error AlreadyRegistered(IWETH registered);
63+
64+ /// @notice Thrown when the candidate's code is not a reviewed wETH.
65+ /// @param candidate The rejected address.
66+ /// @param codeHash Its `EXTCODEHASH`, zero if there is no code there.
67+ error UnknownImplementation(IWETH candidate, bytes32 codeHash);
68+
69+ /// @notice Thrown when the call does not carry exactly `PROBE` wei.
70+ /// @param sent The value that came with the call.
71+ error ProbeValueRequired(uint256 sent);
72+
73+ /// @notice Thrown when wrapping `PROBE` wei did not mint `PROBE` wrapped.
74+ /// @param expected The balance a real wrapper would have produced.
75+ /// @param actual The balance the candidate produced.
76+ error DepositMismatch(uint256 expected, uint256 actual);
77+
78+ /// @notice Thrown when unwrapping did not burn the wrapped token.
79+ /// @param expected The balance a real wrapper would have left behind.
80+ /// @param actual The balance the candidate left behind.
81+ error WithdrawMismatch(uint256 expected, uint256 actual);
82+
83+ /// @notice Thrown when unwrapping did not return the ether.
84+ /// @param expected The ether balance the round trip should have restored.
85+ /// @param actual The ether balance it actually left.
86+ error EtherNotReturned(uint256 expected, uint256 actual);
87+
88+ /// @notice Thrown when ether is sent outside a round trip.
89+ error NotProbing();
90+
91+ /// @notice Emitted once, when a chain's wETH is settled.
92+ /// @param weth The accepted wETH.
93+ /// @param registrar Whoever supplied and paid for it.
94+ /// @param codeHash The allowlisted hash its code matched.
95+ event Registered(IWETH indexed weth, address indexed registrar, bytes32 codeHash);
96+
97+ /// @notice Accept `candidate` as this chain's wETH, if its code is one of
98+ /// the reviewed implementations and it wraps ether correctly.
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+ ///
106+ /// Reverts rather than degrading when the candidate is unrecognised —
107+ /// unreviewed bytecode cannot be shown safe by any test, static or
108+ /// behavioural, so a chain running its own wETH is a chain `Love`
109+ /// does not deploy on until that implementation is reviewed and
110+ /// added.
111+ /// @param candidate The wETH to register.
112+ /// @return The registered wETH, for the convenience of scripts.
113+ function register(IWETH candidate) external payable returns (IWETH) {
114+ if (address(weth) != address(0)) revert AlreadyRegistered(weth);
115+ if (msg.value != PROBE) revert ProbeValueRequired(msg.value);
116+
117+ bytes32 codeHash = address(candidate).codehash;
118+ if (!isKnownImplementation(codeHash)) revert UnknownImplementation(candidate, codeHash);
119+
120+ _probe(candidate);
121+
122+ weth = candidate;
123+ emit Registered(candidate, msg.sender, codeHash);
124+
125+ return candidate;
126+ }
127+
128+ /// @notice Whether `codeHash` is one of the reviewed wETH implementations.
129+ /// @dev Compile-time, so the list is ownerless and append-only by
130+ /// construction: extending it means publishing a new registry, which
131+ /// leaves every existing deployment exactly as it was. No key can
132+ /// revoke an entry and strand a live `Love`.
133+ ///
134+ /// These are exact `EXTCODEHASH` values, metadata included. Hashing
135+ /// the code with solc's trailing metadata stripped would fold each
136+ /// family into a single entry, but it would also accept a known
137+ /// implementation followed by arbitrary appended bytes, and the
138+ /// safety of that rests on control flow never reaching them — a
139+ /// property that holds for every family here and would have to keep
140+ /// holding for every family added later. Ten constants is the cheaper
141+ /// side of that trade.
142+ ///
143+ /// Grouped by implementation. Every value is reproducible from chain
144+ /// state with `script/weth-codehashes.sh`.
145+ /// @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) {
148+ // OP Stack legacy WETH9, 2041 bytes, solc 0.5.17. One implementation,
149+ // four hashes: these chains run byte-identical code and differ only
150+ // 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
160+
161+ // OP Stack WETH, 2865 bytes, solc 0.8.15.
162+ if (codeHash == 0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012) return true; // unichain, soneium, ink
163+
164+ // Taiko, 3204 bytes.
165+ if (codeHash == 0x9f3d95086909fce850d997158aba31abe26c3aad6a413107ca0bf9d53a7c42e9) return true; // taiko
166+
167+ // Scroll, 5871 bytes.
168+ if (codeHash == 0xe8c4073351c26b9831c1e5af153b9be4713a4af9edfdf32b58077b735e120f14) return true; // scroll
169+
170+ return false;
171+ }
172+
173+ /// @notice Take `PROBE` wei through the candidate and back.
174+ /// @dev Costs the registrar nothing but gas: the wei returns. Balances are
175+ /// read before and after rather than assumed to start at zero, so a
176+ /// candidate that was sent wETH beforehand cannot skew the check.
177+ /// @param candidate The wETH being probed.
178+ function _probe(IWETH candidate) private {
179+ uint256 etherBefore = address(this).balance;
180+ uint256 wrappedBefore = candidate.balanceOf(address(this));
181+
182+ _probing = true;
183+
184+ candidate.deposit{value: PROBE}();
185+ uint256 wrapped = candidate.balanceOf(address(this));
186+ if (wrapped != wrappedBefore + PROBE) revert DepositMismatch(wrappedBefore + PROBE, wrapped);
187+
188+ candidate.withdraw(PROBE);
189+ uint256 unwrapped = candidate.balanceOf(address(this));
190+ if (unwrapped != wrappedBefore) revert WithdrawMismatch(wrappedBefore, unwrapped);
191+
192+ _probing = false;
193+
194+ if (address(this).balance != etherBefore) revert EtherNotReturned(etherBefore, address(this).balance);
195+ }
196+
197+ /// @notice Takes the ether a candidate returns mid-probe, and nothing else.
198+ /// @dev The registry is not a wallet. Outside a round trip this reverts,
199+ /// so ether cannot be stranded in a contract with no way to move it.
200+ receive() external payable {
201+ if (!_probing) revert NotProbing();
202+ }
203+}
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 @@
11 // SPDX-License-Identifier: MIT
22 pragma solidity ^0.8.30;
33
4+import {IWETH} from "../src/IWETH.sol";
45 import {Love} from "../src/Love.sol";
5-import {MockWETH} from "./mocks/MockWETH.sol";
6+import {Fixtures} from "./Fixtures.sol";
67 import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol";
78 import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
89 import {Test} from "forge-std/Test.sol";
910
1011 /// @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
1213 /// deposit, redemption on withdraw, the divisibility rule, and the
1314 /// invariant that supply always equals the wETH backing times the rate.
14-contract LoveTest is Test {
15+contract LoveTest is Test, Fixtures {
1516 /// @notice The token under test.
1617 Love public love;
1718
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;
2021
2122 address alice = makeAddr("alice");
2223 address bob = makeAddr("bob");
@@ -24,9 +25,12 @@ contract LoveTest is Test {
2425
2526 uint256 constant RATE = 100_000;
2627
28+ /// @dev The OP Stack predeploy address, so the setup mirrors a real chain.
29+ address constant WETH_AT = 0x4200000000000000000000000000000000000006;
30+
2731 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();
3034
3135 _fund(alice, 10 ether);
3236 _fund(bob, 10 ether);
@@ -115,11 +119,14 @@ contract LoveTest is Test {
115119 assertEq(love.totalSupply(), 0);
116120 }
117121
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.
118127 function test_RevertWhen_DepositWithoutApproval() public {
119128 vm.prank(alice);
120- vm.expectRevert(
121- abi.encodeWithSelector(IERC20Errors.ERC20InsufficientAllowance.selector, address(love), 0, 1 ether)
122- );
129+ vm.expectRevert(bytes(""));
123130 love.deposit(1 ether);
124131 }
125132
@@ -128,9 +135,7 @@ contract LoveTest is Test {
128135 weth.approve(address(love), 100 ether);
129136
130137 vm.prank(alice);
131- vm.expectRevert(
132- abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, alice, 10 ether, 100 ether)
133- );
138+ vm.expectRevert(bytes(""));
134139 love.deposit(100 ether);
135140 }
136141
@@ -1,22 +1,23 @@
1 // SPDX-License-Identifier: MIT1 // 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 LoveTest11 /// @title LoveTest
11-/// @notice Exercises the peg end to end against a WETH9 stand-in: minting on12+/// @notice Exercises the peg end to end against real WETH9 bytecode: minting on
12 /// deposit, redemption on withdraw, the divisibility rule, and the13 /// 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;
33
44 import {LoveScript} from "../script/Love.s.sol";
55 import {Love} from "../src/Love.sol";
6+import {Fixtures} from "./Fixtures.sol";
67 import {Test} from "forge-std/Test.sol";
78
89 /// @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 {
1417 bytes32 constant SALT = keccak256("LOVE");
15- address constant WETH = 0x4200000000000000000000000000000000000006;
18+ address constant OP_STACK_WETH = 0x4200000000000000000000000000000000000006;
19+ address constant MAINNET_WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
1620
1721 LoveScript script;
1822
@@ -23,19 +27,56 @@ contract LoveCreate2Test is Test {
2327 assertGt(CREATE2_FACTORY.code.length, 0, "create2 deployer missing");
2428 }
2529
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+
2664 function test_DeploysAtPredictedAddress() public {
27- address predicted = vm.computeCreate2Address(SALT, keccak256(script.initCode(WETH)), CREATE2_FACTORY);
65+ setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY);
2866
29- Love deployed = Love(_deploy(SALT, WETH));
67+ address predicted = vm.computeCreate2Address(SALT, keccak256(script.initCode()), CREATE2_FACTORY);
68+ Love deployed = Love(_deploy(SALT));
3069
3170 assertEq(address(deployed), predicted);
3271 assertEq(deployed.symbol(), "LOVE");
33- assertEq(address(deployed.WETH()), WETH);
72+ assertEq(address(deployed.WETH()), OP_STACK_WETH);
3473 }
3574
3675 /// @dev What the deploy script prints must be what the chain gives back.
3776 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));
3980 }
4081
4182 function test_ScriptUsesKeccakOfLoveAsDefaultSalt() public view {
@@ -43,46 +84,89 @@ contract LoveCreate2Test is Test {
4384 }
4485
4586 function test_ScriptDefaultsToOpStackWeth() public view {
46- assertEq(script.DEFAULT_WETH(), WETH);
87+ assertEq(script.DEFAULT_WETH(), OP_STACK_WETH);
4788 }
4889
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.
5092 function test_PredictionIsChainAgnostic() public {
51- address onThisChain = script.predict(SALT, WETH);
93+ address onThisChain = script.predict(SALT);
5294
5395 vm.chainId(137);
54- assertEq(script.predict(SALT, WETH), onThisChain);
96+ assertEq(script.predict(SALT), onThisChain);
5597
5698 vm.chainId(42_161);
57- assertEq(script.predict(SALT, WETH), onThisChain);
99+ assertEq(script.predict(SALT), onThisChain);
58100 }
59101
60102 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")));
62104 }
63105
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");
68133 }
69134
70135 /// @dev Redeploying with the same salt must fail, not silently return the
71136 /// existing token.
72137 function test_RevertWhen_RedeployingWithSameSalt() public {
73- _deploy(SALT, WETH);
138+ setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY);
139+ _deploy(SALT);
74140
75- (bool ok,) = CREATE2_FACTORY.call(abi.encodePacked(SALT, script.initCode(WETH)));
141+ (bool ok,) = CREATE2_FACTORY.call(abi.encodePacked(SALT, script.initCode()));
76142 assertFalse(ok);
77143 }
78144
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();
81165 }
82166
83167 /// @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()));
86170 require(ok, "create2 deployment failed");
87171 // casting to 'bytes20' is safe because the deployer returns the 20-byte address, raw
88172 // 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 LoveCreate2Test9 /// @title LoveCreate2Test
9-/// @notice The deployment address derives from the salt, the creation code and10+/// @notice The deployment address now derives from the salt and the creation
10-/// the wETH constructor argument. These tests pin that derivation: same11+/// 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 different12+/// 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 their106+ /*//////////////////////////////////////////////////////////////
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 the135 /// @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, raw171 // 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/WETHRegistry.t.sol +281 -0
new file mode 100644
@@ -0,0 +1,281 @@
1+// SPDX-License-Identifier: MIT
2+pragma solidity ^0.8.30;
3+
4+import {IWETH} from "../src/IWETH.sol";
5+import {WETHRegistry} from "../src/WETHRegistry.sol";
6+import {Fixtures} from "./Fixtures.sol";
7+import {MockWETH} from "./mocks/MockWETH.sol";
8+import {Test} from "forge-std/Test.sol";
9+
10+/// @notice A registrar with no payable fallback, which is what a deployer
11+/// contract or a multisig looks like from the registry's side.
12+contract PlainRegistrar {
13+ function register(WETHRegistry registry, IWETH weth) external payable {
14+ registry.register{value: msg.value}(weth);
15+ }
16+}
17+
18+/// @title WETHRegistryTest
19+/// @notice Covers the two gates the registry exists to run — is this code a
20+/// reviewed wETH, and does it actually wrap — plus the write-once rule
21+/// that stops the token under LOVE's peg from ever being swapped.
22+contract WETHRegistryTest is Test, Fixtures {
23+ WETHRegistry registry;
24+
25+ /// @dev Read once in `setUp`. Calling `registry.PROBE()` inline after
26+ /// `vm.expectRevert` would bind the expectation to that call instead
27+ /// of to `register`.
28+ uint256 probe;
29+
30+ address constant WETH_AT = 0x4200000000000000000000000000000000000006;
31+ address constant OTHER_WETH_AT = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
32+
33+ event Registered(IWETH indexed weth, address indexed registrar, bytes32 codeHash);
34+
35+ function setUp() public {
36+ registry = deployRegistry();
37+ probe = registry.PROBE();
38+ }
39+
40+ /*//////////////////////////////////////////////////////////////
41+ THE ALLOWLIST
42+ //////////////////////////////////////////////////////////////*/
43+
44+ /// @dev The ten entries, reproducible from chain state with
45+ /// `script/weth-codehashes.sh`. Listing them here means a hash edited
46+ /// in the contract shows up as a failing test rather than as a silent
47+ /// change to what LOVE will accept.
48+ function test_KnownImplementations() public view {
49+ bytes32[10] memory allowed = [
50+ // OP Stack legacy WETH9
51+ bytes32(0x779bbf2a738ef09d961c945116197e2ac764c1b39304b2b4418cd4e42668b173),
52+ 0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c,
53+ 0x557c8e14d33f7cd67cad0141e1a49ebf3488a447fc3df7aa66b127778a0383d1,
54+ 0xf35fe602ba2a3b96f2e27ff7c8b8010800a8d0d616a5fb1f902e087b590355f3,
55+ // Canonical WETH9
56+ 0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23,
57+ 0xa670ec6c272ddec6d328d6f3d5cad65a841a6ab45e8e5cf825150eb458be4f1f,
58+ 0x032e9cab14331328530468e54f1b91777b4d5c9dbbb400884badb32bc4113585,
59+ // OP Stack WETH, solc 0.8.15
60+ 0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012,
61+ // Taiko
62+ 0x9f3d95086909fce850d997158aba31abe26c3aad6a413107ca0bf9d53a7c42e9,
63+ // Scroll
64+ 0xe8c4073351c26b9831c1e5af153b9be4713a4af9edfdf32b58077b735e120f14
65+ ];
66+
67+ for (uint256 i = 0; i < allowed.length; i++) {
68+ assertTrue(registry.isKnownImplementation(allowed[i]), "allowlisted hash rejected");
69+ }
70+ }
71+
72+ function test_EmptyCodeHashIsNotKnown() public view {
73+ assertFalse(registry.isKnownImplementation(bytes32(0)));
74+ assertFalse(registry.isKnownImplementation(keccak256("")));
75+ }
76+
77+ function testFuzz_UnknownHashesAreRejected(bytes32 codeHash) public view {
78+ vm.assume(!registry.isKnownImplementation(codeHash));
79+ assertFalse(registry.isKnownImplementation(codeHash));
80+ }
81+
82+ /*//////////////////////////////////////////////////////////////
83+ REGISTERING
84+ //////////////////////////////////////////////////////////////*/
85+
86+ function test_RegistersOpStackWeth() public {
87+ IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
88+ register(registry, weth);
89+
90+ assertEq(address(registry.weth()), WETH_AT);
91+ }
92+
93+ function test_RegistersCanonicalWeth() public {
94+ IWETH weth = etchWeth(OTHER_WETH_AT, WETH9_CANONICAL);
95+ register(registry, weth);
96+
97+ assertEq(address(registry.weth()), OTHER_WETH_AT);
98+ }
99+
100+ function test_EmitsRegistered() public {
101+ IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
102+ deal(address(this), 1 ether);
103+
104+ vm.expectEmit(true, true, true, true);
105+ emit Registered(weth, address(this), WETH_AT.codehash);
106+ registry.register{value: probe}(weth);
107+ }
108+
109+ function test_NothingRegisteredInitially() public view {
110+ assertEq(address(registry.weth()), address(0));
111+ }
112+
113+ /// @dev The reason the probe wei is not refunded: paying it back means
114+ /// calling the registrar with value, which a contract without a
115+ /// payable fallback cannot accept.
116+ function test_RegistrarWithoutPayableFallbackCanRegister() public {
117+ IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
118+ PlainRegistrar registrar = new PlainRegistrar();
119+ deal(address(this), 1 ether);
120+
121+ registrar.register{value: probe}(registry, weth);
122+
123+ assertEq(address(registry.weth()), WETH_AT);
124+ }
125+
126+ function test_ProbeWeiStaysInTheRegistry() public {
127+ IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
128+ register(registry, weth);
129+
130+ assertEq(address(registry).balance, probe);
131+ }
132+
133+ /*//////////////////////////////////////////////////////////////
134+ THE BYTECODE GATE
135+ //////////////////////////////////////////////////////////////*/
136+
137+ /// @dev `MockWETH` wraps ether correctly and would sail through the
138+ /// behavioural probe. It is rejected on its code alone, which is the
139+ /// gate doing exactly what it is for: behaving well today says
140+ /// nothing about what else the code can do.
141+ function test_RevertWhen_CodeIsNotAReviewedImplementation() public {
142+ MockWETH mock = new MockWETH();
143+ deal(address(this), 1 ether);
144+
145+ vm.expectRevert(
146+ abi.encodeWithSelector(
147+ WETHRegistry.UnknownImplementation.selector, IWETH(address(mock)), address(mock).codehash
148+ )
149+ );
150+ registry.register{value: probe}(IWETH(address(mock)));
151+ }
152+
153+ function test_RevertWhen_CandidateHasNoCode() public {
154+ address empty = makeAddr("empty");
155+ deal(address(this), 1 ether);
156+
157+ vm.expectRevert(abi.encodeWithSelector(WETHRegistry.UnknownImplementation.selector, IWETH(empty), bytes32(0)));
158+ registry.register{value: probe}(IWETH(empty));
159+ }
160+
161+ /*//////////////////////////////////////////////////////////////
162+ THE BEHAVIOURAL PROBE
163+ //////////////////////////////////////////////////////////////*/
164+
165+ /// @dev Mocking `balanceOf` leaves the codehash untouched, so the
166+ /// candidate still passes the allowlist and the probe is what has to
167+ /// catch it. A wrapper that mints nothing on deposit fails here.
168+ function test_RevertWhen_DepositMintsNothing() public {
169+ IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
170+ deal(address(this), 1 ether);
171+
172+ vm.mockCall(WETH_AT, abi.encodeCall(weth.balanceOf, (address(registry))), abi.encode(uint256(0)));
173+
174+ vm.expectRevert(abi.encodeWithSelector(WETHRegistry.DepositMismatch.selector, 1, 0));
175+ registry.register{value: probe}(weth);
176+ }
177+
178+ /// @dev Mints on deposit, but burns nothing on withdraw: the balance is
179+ /// read three times, and the third answer is the one that lies.
180+ function test_RevertWhen_WithdrawBurnsNothing() public {
181+ IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
182+ deal(address(this), 1 ether);
183+
184+ bytes[] memory balances = new bytes[](3);
185+ balances[0] = abi.encode(uint256(0)); // before
186+ balances[1] = abi.encode(uint256(1)); // after deposit, correct
187+ balances[2] = abi.encode(uint256(1)); // after withdraw, should be 0
188+ vm.mockCalls(WETH_AT, abi.encodeCall(weth.balanceOf, (address(registry))), balances);
189+
190+ vm.expectRevert(abi.encodeWithSelector(WETHRegistry.WithdrawMismatch.selector, 0, 1));
191+ registry.register{value: probe}(weth);
192+ }
193+
194+ /// @dev Books the withdrawal correctly but keeps the ether. The balance
195+ /// check after the round trip is the only thing that notices.
196+ function test_RevertWhen_WithdrawKeepsTheEther() public {
197+ IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
198+ deal(address(this), 1 ether);
199+
200+ vm.mockCall(WETH_AT, abi.encodeCall(weth.withdraw, (1)), "");
201+
202+ bytes[] memory balances = new bytes[](3);
203+ balances[0] = abi.encode(uint256(0));
204+ balances[1] = abi.encode(uint256(1));
205+ balances[2] = abi.encode(uint256(0));
206+ vm.mockCalls(WETH_AT, abi.encodeCall(weth.balanceOf, (address(registry))), balances);
207+
208+ vm.expectRevert(abi.encodeWithSelector(WETHRegistry.EtherNotReturned.selector, 1, 0));
209+ registry.register{value: probe}(weth);
210+ }
211+
212+ /*//////////////////////////////////////////////////////////////
213+ THE PROBE FEE
214+ //////////////////////////////////////////////////////////////*/
215+
216+ function test_RevertWhen_NoProbeValue() public {
217+ IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
218+
219+ vm.expectRevert(abi.encodeWithSelector(WETHRegistry.ProbeValueRequired.selector, 0));
220+ registry.register(weth);
221+ }
222+
223+ function testFuzz_RevertWhen_WrongProbeValue(uint96 value) public {
224+ vm.assume(value != probe);
225+ IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
226+ deal(address(this), uint256(value) + 1 ether);
227+
228+ vm.expectRevert(abi.encodeWithSelector(WETHRegistry.ProbeValueRequired.selector, value));
229+ registry.register{value: value}(weth);
230+ }
231+
232+ /*//////////////////////////////////////////////////////////////
233+ WRITE-ONCE
234+ //////////////////////////////////////////////////////////////*/
235+
236+ function test_RevertWhen_AlreadyRegistered() public {
237+ IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
238+ register(registry, weth);
239+
240+ deal(address(this), 1 ether);
241+ vm.expectRevert(abi.encodeWithSelector(WETHRegistry.AlreadyRegistered.selector, weth));
242+ registry.register{value: probe}(weth);
243+ }
244+
245+ /// @dev Including with a second, equally valid wETH — the first answer is
246+ /// final, so nothing can move the token under a live peg.
247+ function test_RevertWhen_ReplacingWithAnotherValidWeth() public {
248+ IWETH first = etchWeth(WETH_AT, WETH9_OP_LEGACY);
249+ register(registry, first);
250+
251+ IWETH second = etchWeth(OTHER_WETH_AT, WETH9_CANONICAL);
252+ deal(address(this), 1 ether);
253+
254+ vm.expectRevert(abi.encodeWithSelector(WETHRegistry.AlreadyRegistered.selector, first));
255+ registry.register{value: probe}(second);
256+
257+ assertEq(address(registry.weth()), WETH_AT);
258+ }
259+
260+ /*//////////////////////////////////////////////////////////////
261+ NOT A WALLET
262+ //////////////////////////////////////////////////////////////*/
263+
264+ /// @dev The registry has no owner and no way to move ether out, so it must
265+ /// not accept any outside a probe.
266+ function test_RevertWhen_EtherSentOutsideAProbe() public {
267+ deal(address(this), 1 ether);
268+
269+ vm.expectRevert(WETHRegistry.NotProbing.selector);
270+ payable(address(registry)).transfer(1 ether);
271+ }
272+
273+ function test_RevertWhen_EtherSentAfterRegistration() public {
274+ IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
275+ register(registry, weth);
276+ deal(address(this), 1 ether);
277+
278+ vm.expectRevert(WETHRegistry.NotProbing.selector);
279+ payable(address(registry)).transfer(1 ether);
280+ }
281+}
new file mode 100644
@@ -0,0 +1,281 @@
1+// SPDX-License-Identifier: MIT
2+pragma solidity ^0.8.30;
3+
4+import {IWETH} from "../src/IWETH.sol";
5+import {WETHRegistry} from "../src/WETHRegistry.sol";
6+import {Fixtures} from "./Fixtures.sol";
7+import {MockWETH} from "./mocks/MockWETH.sol";
8+import {Test} from "forge-std/Test.sol";
9+
10+/// @notice A registrar with no payable fallback, which is what a deployer
11+/// contract or a multisig looks like from the registry's side.
12+contract PlainRegistrar {
13+ function register(WETHRegistry registry, IWETH weth) external payable {
14+ registry.register{value: msg.value}(weth);
15+ }
16+}
17+
18+/// @title WETHRegistryTest
19+/// @notice Covers the two gates the registry exists to run — is this code a
20+/// reviewed wETH, and does it actually wrap — plus the write-once rule
21+/// that stops the token under LOVE's peg from ever being swapped.
22+contract WETHRegistryTest is Test, Fixtures {
23+ WETHRegistry registry;
24+
25+ /// @dev Read once in `setUp`. Calling `registry.PROBE()` inline after
26+ /// `vm.expectRevert` would bind the expectation to that call instead
27+ /// of to `register`.
28+ uint256 probe;
29+
30+ address constant WETH_AT = 0x4200000000000000000000000000000000000006;
31+ address constant OTHER_WETH_AT = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
32+
33+ event Registered(IWETH indexed weth, address indexed registrar, bytes32 codeHash);
34+
35+ function setUp() public {
36+ registry = deployRegistry();
37+ probe = registry.PROBE();
38+ }
39+
40+ /*//////////////////////////////////////////////////////////////
41+ THE ALLOWLIST
42+ //////////////////////////////////////////////////////////////*/
43+
44+ /// @dev The ten entries, reproducible from chain state with
45+ /// `script/weth-codehashes.sh`. Listing them here means a hash edited
46+ /// in the contract shows up as a failing test rather than as a silent
47+ /// change to what LOVE will accept.
48+ function test_KnownImplementations() public view {
49+ bytes32[10] memory allowed = [
50+ // OP Stack legacy WETH9
51+ bytes32(0x779bbf2a738ef09d961c945116197e2ac764c1b39304b2b4418cd4e42668b173),
52+ 0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c,
53+ 0x557c8e14d33f7cd67cad0141e1a49ebf3488a447fc3df7aa66b127778a0383d1,
54+ 0xf35fe602ba2a3b96f2e27ff7c8b8010800a8d0d616a5fb1f902e087b590355f3,
55+ // Canonical WETH9
56+ 0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23,
57+ 0xa670ec6c272ddec6d328d6f3d5cad65a841a6ab45e8e5cf825150eb458be4f1f,
58+ 0x032e9cab14331328530468e54f1b91777b4d5c9dbbb400884badb32bc4113585,
59+ // OP Stack WETH, solc 0.8.15
60+ 0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012,
61+ // Taiko
62+ 0x9f3d95086909fce850d997158aba31abe26c3aad6a413107ca0bf9d53a7c42e9,
63+ // Scroll
64+ 0xe8c4073351c26b9831c1e5af153b9be4713a4af9edfdf32b58077b735e120f14
65+ ];
66+
67+ for (uint256 i = 0; i < allowed.length; i++) {
68+ assertTrue(registry.isKnownImplementation(allowed[i]), "allowlisted hash rejected");
69+ }
70+ }
71+
72+ function test_EmptyCodeHashIsNotKnown() public view {
73+ assertFalse(registry.isKnownImplementation(bytes32(0)));
74+ assertFalse(registry.isKnownImplementation(keccak256("")));
75+ }
76+
77+ function testFuzz_UnknownHashesAreRejected(bytes32 codeHash) public view {
78+ vm.assume(!registry.isKnownImplementation(codeHash));
79+ assertFalse(registry.isKnownImplementation(codeHash));
80+ }
81+
82+ /*//////////////////////////////////////////////////////////////
83+ REGISTERING
84+ //////////////////////////////////////////////////////////////*/
85+
86+ function test_RegistersOpStackWeth() public {
87+ IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
88+ register(registry, weth);
89+
90+ assertEq(address(registry.weth()), WETH_AT);
91+ }
92+
93+ function test_RegistersCanonicalWeth() public {
94+ IWETH weth = etchWeth(OTHER_WETH_AT, WETH9_CANONICAL);
95+ register(registry, weth);
96+
97+ assertEq(address(registry.weth()), OTHER_WETH_AT);
98+ }
99+
100+ function test_EmitsRegistered() public {
101+ IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
102+ deal(address(this), 1 ether);
103+
104+ vm.expectEmit(true, true, true, true);
105+ emit Registered(weth, address(this), WETH_AT.codehash);
106+ registry.register{value: probe}(weth);
107+ }
108+
109+ function test_NothingRegisteredInitially() public view {
110+ assertEq(address(registry.weth()), address(0));
111+ }
112+
113+ /// @dev The reason the probe wei is not refunded: paying it back means
114+ /// calling the registrar with value, which a contract without a
115+ /// payable fallback cannot accept.
116+ function test_RegistrarWithoutPayableFallbackCanRegister() public {
117+ IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
118+ PlainRegistrar registrar = new PlainRegistrar();
119+ deal(address(this), 1 ether);
120+
121+ registrar.register{value: probe}(registry, weth);
122+
123+ assertEq(address(registry.weth()), WETH_AT);
124+ }
125+
126+ function test_ProbeWeiStaysInTheRegistry() public {
127+ IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
128+ register(registry, weth);
129+
130+ assertEq(address(registry).balance, probe);
131+ }
132+
133+ /*//////////////////////////////////////////////////////////////
134+ THE BYTECODE GATE
135+ //////////////////////////////////////////////////////////////*/
136+
137+ /// @dev `MockWETH` wraps ether correctly and would sail through the
138+ /// behavioural probe. It is rejected on its code alone, which is the
139+ /// gate doing exactly what it is for: behaving well today says
140+ /// nothing about what else the code can do.
141+ function test_RevertWhen_CodeIsNotAReviewedImplementation() public {
142+ MockWETH mock = new MockWETH();
143+ deal(address(this), 1 ether);
144+
145+ vm.expectRevert(
146+ abi.encodeWithSelector(
147+ WETHRegistry.UnknownImplementation.selector, IWETH(address(mock)), address(mock).codehash
148+ )
149+ );
150+ registry.register{value: probe}(IWETH(address(mock)));
151+ }
152+
153+ function test_RevertWhen_CandidateHasNoCode() public {
154+ address empty = makeAddr("empty");
155+ deal(address(this), 1 ether);
156+
157+ vm.expectRevert(abi.encodeWithSelector(WETHRegistry.UnknownImplementation.selector, IWETH(empty), bytes32(0)));
158+ registry.register{value: probe}(IWETH(empty));
159+ }
160+
161+ /*//////////////////////////////////////////////////////////////
162+ THE BEHAVIOURAL PROBE
163+ //////////////////////////////////////////////////////////////*/
164+
165+ /// @dev Mocking `balanceOf` leaves the codehash untouched, so the
166+ /// candidate still passes the allowlist and the probe is what has to
167+ /// catch it. A wrapper that mints nothing on deposit fails here.
168+ function test_RevertWhen_DepositMintsNothing() public {
169+ IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
170+ deal(address(this), 1 ether);
171+
172+ vm.mockCall(WETH_AT, abi.encodeCall(weth.balanceOf, (address(registry))), abi.encode(uint256(0)));
173+
174+ vm.expectRevert(abi.encodeWithSelector(WETHRegistry.DepositMismatch.selector, 1, 0));
175+ registry.register{value: probe}(weth);
176+ }
177+
178+ /// @dev Mints on deposit, but burns nothing on withdraw: the balance is
179+ /// read three times, and the third answer is the one that lies.
180+ function test_RevertWhen_WithdrawBurnsNothing() public {
181+ IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
182+ deal(address(this), 1 ether);
183+
184+ bytes[] memory balances = new bytes[](3);
185+ balances[0] = abi.encode(uint256(0)); // before
186+ balances[1] = abi.encode(uint256(1)); // after deposit, correct
187+ balances[2] = abi.encode(uint256(1)); // after withdraw, should be 0
188+ vm.mockCalls(WETH_AT, abi.encodeCall(weth.balanceOf, (address(registry))), balances);
189+
190+ vm.expectRevert(abi.encodeWithSelector(WETHRegistry.WithdrawMismatch.selector, 0, 1));
191+ registry.register{value: probe}(weth);
192+ }
193+
194+ /// @dev Books the withdrawal correctly but keeps the ether. The balance
195+ /// check after the round trip is the only thing that notices.
196+ function test_RevertWhen_WithdrawKeepsTheEther() public {
197+ IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
198+ deal(address(this), 1 ether);
199+
200+ vm.mockCall(WETH_AT, abi.encodeCall(weth.withdraw, (1)), "");
201+
202+ bytes[] memory balances = new bytes[](3);
203+ balances[0] = abi.encode(uint256(0));
204+ balances[1] = abi.encode(uint256(1));
205+ balances[2] = abi.encode(uint256(0));
206+ vm.mockCalls(WETH_AT, abi.encodeCall(weth.balanceOf, (address(registry))), balances);
207+
208+ vm.expectRevert(abi.encodeWithSelector(WETHRegistry.EtherNotReturned.selector, 1, 0));
209+ registry.register{value: probe}(weth);
210+ }
211+
212+ /*//////////////////////////////////////////////////////////////
213+ THE PROBE FEE
214+ //////////////////////////////////////////////////////////////*/
215+
216+ function test_RevertWhen_NoProbeValue() public {
217+ IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
218+
219+ vm.expectRevert(abi.encodeWithSelector(WETHRegistry.ProbeValueRequired.selector, 0));
220+ registry.register(weth);
221+ }
222+
223+ function testFuzz_RevertWhen_WrongProbeValue(uint96 value) public {
224+ vm.assume(value != probe);
225+ IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
226+ deal(address(this), uint256(value) + 1 ether);
227+
228+ vm.expectRevert(abi.encodeWithSelector(WETHRegistry.ProbeValueRequired.selector, value));
229+ registry.register{value: value}(weth);
230+ }
231+
232+ /*//////////////////////////////////////////////////////////////
233+ WRITE-ONCE
234+ //////////////////////////////////////////////////////////////*/
235+
236+ function test_RevertWhen_AlreadyRegistered() public {
237+ IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
238+ register(registry, weth);
239+
240+ deal(address(this), 1 ether);
241+ vm.expectRevert(abi.encodeWithSelector(WETHRegistry.AlreadyRegistered.selector, weth));
242+ registry.register{value: probe}(weth);
243+ }
244+
245+ /// @dev Including with a second, equally valid wETH — the first answer is
246+ /// final, so nothing can move the token under a live peg.
247+ function test_RevertWhen_ReplacingWithAnotherValidWeth() public {
248+ IWETH first = etchWeth(WETH_AT, WETH9_OP_LEGACY);
249+ register(registry, first);
250+
251+ IWETH second = etchWeth(OTHER_WETH_AT, WETH9_CANONICAL);
252+ deal(address(this), 1 ether);
253+
254+ vm.expectRevert(abi.encodeWithSelector(WETHRegistry.AlreadyRegistered.selector, first));
255+ registry.register{value: probe}(second);
256+
257+ assertEq(address(registry.weth()), WETH_AT);
258+ }
259+
260+ /*//////////////////////////////////////////////////////////////
261+ NOT A WALLET
262+ //////////////////////////////////////////////////////////////*/
263+
264+ /// @dev The registry has no owner and no way to move ether out, so it must
265+ /// not accept any outside a probe.
266+ function test_RevertWhen_EtherSentOutsideAProbe() public {
267+ deal(address(this), 1 ether);
268+
269+ vm.expectRevert(WETHRegistry.NotProbing.selector);
270+ payable(address(registry)).transfer(1 ether);
271+ }
272+
273+ function test_RevertWhen_EtherSentAfterRegistration() public {
274+ IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY);
275+ register(registry, weth);
276+ deal(address(this), 1 ether);
277+
278+ vm.expectRevert(WETHRegistry.NotProbing.selector);
279+ payable(address(registry)).transfer(1 ether);
280+ }
281+}
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