document the registry
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 | ||
| 7 | 7 | |
| 8 | 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 | 30 | ### 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 | 51 | - Foundry project scaffolding (`forge-std`, CI workflow, `.gitignore`). |
| 13 | 52 | - `Love`, an ERC-20 token (`LOVE`, 18 decimals) on OpenZeppelin v5, pegged to |
| 14 | 53 | 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 | 60 | approve/`transferFrom`, reverts and fuzzing. |
| 22 | 61 | - Deployment script `script/Love.s.sol`, deploying with CREATE2 through the |
| 23 | 62 | 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. | |
| 27 | 66 | - `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. | |
| 30 | 69 | - `foundry.toml` with pinned solc 0.8.30, optimizer, fmt rules, fuzz/invariant |
| 31 | 70 | defaults, and Base Sepolia RPC/Etherscan endpoints driven by env vars. |
| 32 | 71 | - `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 | ### Added | 30 | ### 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 to | 52 | - `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 moves | 53 | 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 the | 61 | - Deployment script `script/Love.s.sol`, deploying with CREATE2 through the |
| 23 | canonical deterministic deployer; salt defaults to `keccak256("LOVE")` and is | 62 | canonical deterministic deployer; salt defaults to `keccak256("LOVE")` and is |
| 24 | - overridable via `SALT`, wETH defaults to the OP-Stack address and is | 63 | + 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 is | 64 | + 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 its | 67 | + 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/invariant | 69 | - `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 with | 71 | - `script/multichain-check.sh`, deploying `Love` on two local anvil nodes with |
modified
README.md +39 -2 | @@ -19,13 +19,46 @@ forge fmt # format | ||
| 19 | 19 | 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 | 52 | ## Deploy |
| 23 | 53 | |
| 24 | 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 | 58 | ```shell |
| 27 | 59 | forge script script/Love.s.sol:LoveScript --sig 'predict()' # print the address |
| 28 | 60 | |
| 61 | +WETH=0x4200000000000000000000000000000000000006 \ | |
| 29 | 62 | forge script script/Love.s.sol:LoveScript \ |
| 30 | 63 | --rpc-url base_sepolia \ |
| 31 | 64 | --account "$DEPLOYER_ACCOUNT" \ |
| @@ -33,14 +66,18 @@ forge script script/Love.s.sol:LoveScript \ | ||
| 33 | 66 | --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 | 72 | To check determinism for real, deploy on two local chains and compare: |
| 37 | 73 | |
| 38 | 74 | ```shell |
| 39 | 75 | ./script/multichain-check.sh |
| 40 | 76 | ``` |
| 41 | 77 | |
| 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`, | |
| 44 | 81 | `CHAIN_ID_A`, `CHAIN_ID_B` or `SALT` as needed. |
| 45 | 82 | |
| 46 | 83 | ## License |
| @@ -19,13 +19,46 @@ forge fmt # format | |||
| 19 | anvil # local node | 19 | 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 | ## Deploy | 52 | ## 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 | ```shell | 58 | ```shell |
| 27 | forge script script/Love.s.sol:LoveScript --sig 'predict()' # print the address | 59 | 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 | --verify | 66 | --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 | ```shell | 74 | ```shell |
| 39 | ./script/multichain-check.sh | 75 | ./script/multichain-check.sh |
| 40 | ``` | 76 | ``` |
| 41 | 77 | ||
| 42 | -It starts two anvil nodes (chain IDs 31337 and 31338), broadcasts the script on | 78 | +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 | ## License | 83 | ## License |