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

document the registry

julienbrg committed 2026-09-17T23:43:47+02:00 Browse files
46722e4 parent: 2f48480
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 -2
@@ -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,14 +66,18 @@ 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
4683 ## License
@@ -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,14 +66,18 @@ 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 ## License83 ## License