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

Verify wETH by bytecode and deploy at one address on every chain #4

Merged@julien wants to merge 5-weth-bytecode-verification into main
Merged as 2f6da5ed8829.
18 files changed
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
Large diff collapsed to keep the page fast — load it when you need it.
added src/WETHRegistry.sol +203 -0
Large diff collapsed to keep the page fast — load it when you need it.
added test/Fixtures.sol +78 -0
Large diff collapsed to keep the page fast — load it when you need it.
modified test/Love.t.sol +18 -13
Large diff collapsed to keep the page fast — load it when you need it.
modified test/LoveCreate2.t.sol +110 -26
Large diff collapsed to keep the page fast — load it when you need it.
added test/WETHRegistry.t.sol +281 -0
Large diff collapsed to keep the page fast — load it when you need it.
added test/fixtures/README.md +22 -0
Large diff collapsed to keep the page fast — load it when you need it.
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