Merge 5-weth-bytecode-verification into main
2f6da5e parent: ce13509 modified
.gitignore +2 -2 | @@ -8,8 +8,8 @@ out/ | ||
| 8 | 8 | /broadcast/*/31338/ |
| 9 | 9 | /broadcast/**/dry-run/ |
| 10 | 10 | |
| 11 | -# Docs | |
| 12 | -docs/ | |
| 11 | +# Docs — `forge doc` output only; docs/notes is written by hand and tracked | |
| 12 | +docs/notes | |
| 13 | 13 | |
| 14 | 14 | # Dotenv file |
| 15 | 15 | .env |
| @@ -8,8 +8,8 @@ out/ | |||
| 8 | /broadcast/*/31338/ | 8 | /broadcast/*/31338/ |
| 9 | /broadcast/**/dry-run/ | 9 | /broadcast/**/dry-run/ |
| 10 | 10 | ||
| 11 | -# Docs | 11 | +# Docs — `forge doc` output only; docs/notes is written by hand and tracked |
| 12 | -docs/ | 12 | +docs/notes |
| 13 | 13 | ||
| 14 | # Dotenv file | 14 | # Dotenv file |
| 15 | .env | 15 | .env |
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 -10 | @@ -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,24 +66,20 @@ 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 | -## 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 | 83 | ## License |
| 55 | 84 | |
| 56 | 85 | GPL-3.0 |
| @@ -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,24 +66,20 @@ 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 | -## 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 | ## License | 83 | ## License |
| 55 | 84 | ||
| 56 | GPL-3.0 | 85 | GPL-3.0 |
modified
foundry.toml +4 -1 | @@ -11,7 +11,10 @@ optimizer_runs = 200 | ||
| 11 | 11 | via_ir = false |
| 12 | 12 | bytecode_hash = "none" |
| 13 | 13 | 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 | 19 | [profile.ci] |
| 17 | 20 | fuzz = { runs = 10_000 } |
| @@ -11,7 +11,10 @@ optimizer_runs = 200 | |||
| 11 | via_ir = false | 11 | via_ir = false |
| 12 | bytecode_hash = "none" | 12 | bytecode_hash = "none" |
| 13 | verbosity = 2 | 13 | 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 @@ | ||
| 1 | 1 | // SPDX-License-Identifier: MIT |
| 2 | 2 | pragma solidity ^0.8.30; |
| 3 | 3 | |
| 4 | +import {IWETH} from "../src/IWETH.sol"; | |
| 4 | 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 | 7 | import {Script, console} from "forge-std/Script.sol"; |
| 7 | 8 | |
| 8 | 9 | /// @title LoveScript |
| 9 | 10 | /// @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. | |
| 16 | 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 | 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 | 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 | 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 | 36 | /// @notice The token, once `run()` has deployed or found it. |
| 25 | 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 | 41 | /// @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 | 43 | /// @return The deployed token. |
| 31 | 44 | function run() public returns (Love) { |
| 32 | - bytes32 s = salt(); | |
| 33 | 45 | address w = wethAddress(); |
| 34 | - address predicted = predict(s, w); | |
| 46 | + address predictedRegistry = registryAddress(); | |
| 47 | + address predicted = predict(salt()); | |
| 35 | 48 | |
| 36 | 49 | console.log("deployer ", CREATE2_FACTORY); |
| 37 | - console.log("salt ", vm.toString(s)); | |
| 50 | + console.log("registry ", predictedRegistry); | |
| 38 | 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 | 54 | console.log("predicted ", predicted); |
| 41 | 55 | |
| 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); | |
| 60 | 59 | |
| 61 | 60 | return love; |
| 62 | 61 | } |
| 63 | 62 | |
| 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. | |
| 66 | 65 | function predict() public view returns (address predicted) { |
| 67 | - bytes32 s = salt(); | |
| 68 | - address w = wethAddress(); | |
| 69 | - predicted = predict(s, w); | |
| 66 | + predicted = predict(salt()); | |
| 70 | 67 | |
| 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())); | |
| 74 | 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 | 79 | /// @param s The CREATE2 salt. |
| 79 | - /// @param w The wETH the token would be pegged to. | |
| 80 | 80 | /// @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); | |
| 83 | 89 | } |
| 84 | 90 | |
| 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; | |
| 90 | 95 | } |
| 91 | 96 | |
| 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()); | |
| 97 | 101 | } |
| 98 | 102 | |
| 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. | |
| 101 | 112 | /// @return The configured salt, or `DEFAULT_SALT`. |
| 102 | 113 | function salt() public view returns (bytes32) { |
| 103 | 114 | return vm.envOr("SALT", DEFAULT_SALT); |
| 104 | 115 | } |
| 105 | 116 | |
| 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. | |
| 108 | 120 | /// @return The configured wETH address, or `DEFAULT_WETH`. |
| 109 | 121 | function wethAddress() public view returns (address) { |
| 110 | 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 | } |
| @@ -1,112 +1,188 @@ | |||
| 1 | // SPDX-License-Identifier: MIT | 1 | // 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 LoveScript | 9 | /// @title LoveScript |
| 9 | /// @author Julien Béranger | 10 | /// @author Julien Béranger |
| 10 | -/// @notice Deploys `Love` with CREATE2 through the canonical deterministic | 11 | +/// @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, and | 12 | +/// registry first, wETH registered second, token third. |
| 12 | -/// the creation code embeds the wETH address — so the token only gets | 13 | +/// @dev Both contracts go through the canonical deterministic deployer and |
| 13 | -/// the same address on chains where wETH sits at the same address. Keep | 14 | +/// neither takes constructor arguments, so their creation code is |
| 14 | -/// `solc`, the optimizer settings and `bytecode_hash = "none"` as they | 15 | +/// 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 the | 41 | /// @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 @@ | ||
| 1 | 1 | #!/usr/bin/env bash |
| 2 | 2 | # |
| 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. | |
| 7 | 16 | # |
| 8 | 17 | # Usage: ./script/multichain-check.sh |
| 9 | 18 | # |
| 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 | |
| 11 | 20 | |
| 12 | 21 | set -euo pipefail |
| 13 | 22 | |
| @@ -20,7 +29,15 @@ CHAIN_ID_B=${CHAIN_ID_B:-31338} | ||
| 20 | 29 | ANVIL_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 |
| 21 | 30 | 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 | 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 | 41 | RPC_A="http://127.0.0.1:$PORT_A" |
| 25 | 42 | RPC_B="http://127.0.0.1:$PORT_B" |
| 26 | 43 | TMP=$(mktemp -d) |
| @@ -53,9 +70,29 @@ start_node() { | ||
| 53 | 70 | 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 | 92 | 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 \ | |
| 59 | 96 | --rpc-url "$rpc" \ |
| 60 | 97 | --private-key "$ANVIL_KEY" \ |
| 61 | 98 | --broadcast \ |
| @@ -72,6 +109,7 @@ deploy() { | ||
| 72 | 109 | report() { |
| 73 | 110 | local rpc=$1 addr=$2 label=$3 |
| 74 | 111 | local code |
| 112 | + | |
| 75 | 113 | code=$(cast code "$addr" --rpc-url "$rpc") |
| 76 | 114 | |
| 77 | 115 | if [[ ${#code} -le 2 ]]; then |
| @@ -79,11 +117,12 @@ report() { | ||
| 79 | 117 | exit 1 |
| 80 | 118 | fi |
| 81 | 119 | |
| 82 | - printf '%s %s %s bytes %s / %s weth %s\n' \ | |
| 120 | + printf '%s %s %s bytes %s / %s\n' \ | |
| 83 | 121 | "$label" "$addr" "$(((${#code} - 2) / 2))" \ |
| 84 | 122 | "$(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")" | |
| 87 | 126 | } |
| 88 | 127 | |
| 89 | 128 | cd "$ROOT" |
| @@ -106,19 +145,37 @@ for pair in "A|$RPC_A" "B|$RPC_B"; do | ||
| 106 | 145 | fi |
| 107 | 146 | done |
| 108 | 147 | |
| 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 | |
| 110 | 155 | |
| 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") | |
| 113 | 158 | |
| 114 | -echo "chain $CHAIN_ID_A" | |
| 159 | +echo "chain $CHAIN_ID_A — OP Stack legacy WETH9 at $WETH_A" | |
| 115 | 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 | 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" ]]; then | |
| 176 | +if [[ $(lower "$ADDR_A") != "$(lower "$ADDR_B")" ]]; then | |
| 120 | 177 | echo "FAIL: addresses differ" >&2 |
| 121 | 178 | exit 1 |
| 122 | 179 | fi |
| 123 | 180 | |
| 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 bash | 1 | #!/usr/bin/env bash |
| 2 | # | 2 | # |
| 3 | -# Deploys Love on two local anvil nodes with different chain ids and checks | 3 | +# 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 the | 4 | +# 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.sh | 17 | # Usage: ./script/multichain-check.sh |
| 9 | # | 18 | # |
| 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 |
| 11 | 20 | ||
| 12 | set -euo pipefail | 21 | set -euo pipefail |
| 13 | 22 | ||
| @@ -20,7 +29,15 @@ CHAIN_ID_B=${CHAIN_ID_B:-31338} | |||
| 20 | ANVIL_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 | 29 | ANVIL_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 |
| 21 | CREATE2_FACTORY=0x4e59b44847b379578588920cA78FbF26c0B4956C | 30 | 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 1 | 70 | 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=$2 | 93 | + 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=$3 | 110 | local rpc=$1 addr=$2 label=$3 |
| 74 | local code | 111 | 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 ]]; then | 115 | if [[ ${#code} -le 2 ]]; then |
| @@ -79,11 +117,12 @@ report() { | |||
| 79 | exit 1 | 117 | exit 1 |
| 80 | fi | 118 | 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 | fi | 145 | fi |
| 107 | done | 146 | done |
| 108 | 147 | ||
| 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 | ||
| 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" ]]; then | 176 | +if [[ $(lower "$ADDR_A") != "$(lower "$ADDR_B")" ]]; then |
| 120 | echo "FAIL: addresses differ" >&2 | 177 | echo "FAIL: addresses differ" >&2 |
| 121 | exit 1 | 178 | exit 1 |
| 122 | fi | 179 | fi |
| 123 | 180 | ||
| 124 | -echo "OK: same address on both chains" | 181 | +echo "OK: same LOVE address on both chains, backed by different wETH" |
added
script/weth-chains.tsv +36 -0 | new file mode 100644 | ||
| @@ -0,0 +1,36 @@ | ||
| 1 | +# The wETH deployment on each chain, and a public RPC to read it from. | |
| 2 | +# | |
| 3 | +# Input to ./weth-codehashes.sh, which hashes the code at each address to find | |
| 4 | +# out how many distinct wETH implementations are actually in use. Entries are | |
| 5 | +# claims, not facts: the script calls name/symbol/decimals on each one and | |
| 6 | +# flags anything that does not answer like wETH, so a wrong address here shows | |
| 7 | +# up as a bad row rather than as a bogus hash in the allowlist. | |
| 8 | +# | |
| 9 | +# chain_id name weth rpc | |
| 10 | +1 ethereum 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 https://ethereum-rpc.publicnode.com | |
| 11 | +10 optimism 0x4200000000000000000000000000000000000006 https://optimism-rpc.publicnode.com | |
| 12 | +56 bnb-chain 0x2170Ed0880ac9A755fd29B2688956BD959F933F8 https://bsc-rpc.publicnode.com | |
| 13 | +100 gnosis 0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1 https://gnosis-rpc.publicnode.com | |
| 14 | +130 unichain 0x4200000000000000000000000000000000000006 https://unichain-rpc.publicnode.com | |
| 15 | +137 polygon 0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619 https://polygon-bor-rpc.publicnode.com | |
| 16 | +204 opbnb 0xE7798f023fC62146e8Aa1b36Da45fb70855a77Ea https://opbnb-rpc.publicnode.com | |
| 17 | +252 fraxtal 0xFC00000000000000000000000000000000000006 https://fraxtal.drpc.org | |
| 18 | +324 zksync-era 0x5AEa5775959fBC2557Cc8789bC1bf90A239D9a91 https://mainnet.era.zksync.io | |
| 19 | +480 world-chain 0x4200000000000000000000000000000000000006 https://worldchain-mainnet.g.alchemy.com/public | |
| 20 | +1088 metis 0x420000000000000000000000000000000000000A https://andromeda.metis.io/?owner=1088 | |
| 21 | +1101 polygon-zkevm 0x4F9A0e7FD2Bf6067db6994CF12E4495Df938E6e9 https://polygon-zkevm.drpc.org | |
| 22 | +1135 lisk 0x4200000000000000000000000000000000000006 https://rpc.api.lisk.com | |
| 23 | +1868 soneium 0x4200000000000000000000000000000000000006 https://rpc.soneium.org | |
| 24 | +5000 mantle 0xdEAddEaDdeadDEadDEADDEAdDEADDEaDDEAD1111 https://rpc.mantle.xyz | |
| 25 | +8453 base 0x4200000000000000000000000000000000000006 https://base-rpc.publicnode.com | |
| 26 | +34443 mode 0x4200000000000000000000000000000000000006 https://mode.drpc.org | |
| 27 | +42161 arbitrum-one 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1 https://arbitrum-one-rpc.publicnode.com | |
| 28 | +42220 celo 0xD221812de1BD094f35587EE8E174B07B6167D9Af https://forno.celo.org | |
| 29 | +43114 avalanche 0x49D5c2BdFfac6CE2BFdB6640F4F80f226bc10bAB https://avalanche-c-chain-rpc.publicnode.com | |
| 30 | +57073 ink 0x4200000000000000000000000000000000000006 https://ink.drpc.org | |
| 31 | +59144 linea 0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f https://rpc.linea.build | |
| 32 | +80094 berachain 0x2F6F07CDcf3588944Bf4C42aC74ff24bF56e7590 https://rpc.berachain.com | |
| 33 | +81457 blast 0x4300000000000000000000000000000000000004 https://blast.drpc.org | |
| 34 | +167000 taiko 0xA51894664A773981C6C112C43ce576f315d5b1B6 https://rpc.mainnet.taiko.xyz | |
| 35 | +534352 scroll 0x5300000000000000000000000000000000000004 https://scroll.drpc.org | |
| 36 | +7777777 zora 0x4200000000000000000000000000000000000006 https://rpc.zora.energy | |
| new file mode 100644 | |||
| @@ -0,0 +1,36 @@ | |||
| 1 | +# The wETH deployment on each chain, and a public RPC to read it from. | ||
| 2 | +# | ||
| 3 | +# Input to ./weth-codehashes.sh, which hashes the code at each address to find | ||
| 4 | +# out how many distinct wETH implementations are actually in use. Entries are | ||
| 5 | +# claims, not facts: the script calls name/symbol/decimals on each one and | ||
| 6 | +# flags anything that does not answer like wETH, so a wrong address here shows | ||
| 7 | +# up as a bad row rather than as a bogus hash in the allowlist. | ||
| 8 | +# | ||
| 9 | +# chain_id name weth rpc | ||
| 10 | +1 ethereum 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 https://ethereum-rpc.publicnode.com | ||
| 11 | +10 optimism 0x4200000000000000000000000000000000000006 https://optimism-rpc.publicnode.com | ||
| 12 | +56 bnb-chain 0x2170Ed0880ac9A755fd29B2688956BD959F933F8 https://bsc-rpc.publicnode.com | ||
| 13 | +100 gnosis 0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1 https://gnosis-rpc.publicnode.com | ||
| 14 | +130 unichain 0x4200000000000000000000000000000000000006 https://unichain-rpc.publicnode.com | ||
| 15 | +137 polygon 0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619 https://polygon-bor-rpc.publicnode.com | ||
| 16 | +204 opbnb 0xE7798f023fC62146e8Aa1b36Da45fb70855a77Ea https://opbnb-rpc.publicnode.com | ||
| 17 | +252 fraxtal 0xFC00000000000000000000000000000000000006 https://fraxtal.drpc.org | ||
| 18 | +324 zksync-era 0x5AEa5775959fBC2557Cc8789bC1bf90A239D9a91 https://mainnet.era.zksync.io | ||
| 19 | +480 world-chain 0x4200000000000000000000000000000000000006 https://worldchain-mainnet.g.alchemy.com/public | ||
| 20 | +1088 metis 0x420000000000000000000000000000000000000A https://andromeda.metis.io/?owner=1088 | ||
| 21 | +1101 polygon-zkevm 0x4F9A0e7FD2Bf6067db6994CF12E4495Df938E6e9 https://polygon-zkevm.drpc.org | ||
| 22 | +1135 lisk 0x4200000000000000000000000000000000000006 https://rpc.api.lisk.com | ||
| 23 | +1868 soneium 0x4200000000000000000000000000000000000006 https://rpc.soneium.org | ||
| 24 | +5000 mantle 0xdEAddEaDdeadDEadDEADDEAdDEADDEaDDEAD1111 https://rpc.mantle.xyz | ||
| 25 | +8453 base 0x4200000000000000000000000000000000000006 https://base-rpc.publicnode.com | ||
| 26 | +34443 mode 0x4200000000000000000000000000000000000006 https://mode.drpc.org | ||
| 27 | +42161 arbitrum-one 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1 https://arbitrum-one-rpc.publicnode.com | ||
| 28 | +42220 celo 0xD221812de1BD094f35587EE8E174B07B6167D9Af https://forno.celo.org | ||
| 29 | +43114 avalanche 0x49D5c2BdFfac6CE2BFdB6640F4F80f226bc10bAB https://avalanche-c-chain-rpc.publicnode.com | ||
| 30 | +57073 ink 0x4200000000000000000000000000000000000006 https://ink.drpc.org | ||
| 31 | +59144 linea 0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f https://rpc.linea.build | ||
| 32 | +80094 berachain 0x2F6F07CDcf3588944Bf4C42aC74ff24bF56e7590 https://rpc.berachain.com | ||
| 33 | +81457 blast 0x4300000000000000000000000000000000000004 https://blast.drpc.org | ||
| 34 | +167000 taiko 0xA51894664A773981C6C112C43ce576f315d5b1B6 https://rpc.mainnet.taiko.xyz | ||
| 35 | +534352 scroll 0x5300000000000000000000000000000000000004 https://scroll.drpc.org | ||
| 36 | +7777777 zora 0x4200000000000000000000000000000000000006 https://rpc.zora.energy | ||
added
script/weth-codehashes.sh +163 -0 | new file mode 100755 | ||
| @@ -0,0 +1,163 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +# | |
| 3 | +# Surveys the wETH deployment on every chain in weth-chains.tsv and groups them | |
| 4 | +# by implementation, to answer the question the allowlist in | |
| 5 | +# src/WETHRegistry.sol depends on: how many distinct wETH implementations are | |
| 6 | +# actually out there? | |
| 7 | +# | |
| 8 | +# For each chain it reports three things: | |
| 9 | +# | |
| 10 | +# codehash keccak256 of the runtime code — exactly what EXTCODEHASH returns | |
| 11 | +# and what the registry compares against, so these values are the | |
| 12 | +# allowlist entries verbatim. | |
| 13 | +# family the same hash with solc's trailing metadata blob removed. That | |
| 14 | +# blob is never executed and embeds a hash of the source path and | |
| 15 | +# comments, so one implementation compiled twice gets two | |
| 16 | +# codehashes but one family. Grouping by family is how the exact | |
| 17 | +# hashes get reviewed together; the registry itself never strips. | |
| 18 | +# kind whether the code carries the ERC-20 and wrapper entrypoints | |
| 19 | +# itself. A bridged-ETH token has no deposit/withdraw, and a proxy | |
| 20 | +# has no entrypoints at all — its codehash commits to a forwarder, | |
| 21 | +# not to behaviour, so it must never be allowlisted. | |
| 22 | +# | |
| 23 | +# Chains are read with eth_getCode and hashed locally rather than with | |
| 24 | +# eth_getProof, which several public RPCs do not serve. | |
| 25 | +# | |
| 26 | +# Usage: ./script/weth-codehashes.sh [chains.tsv] | |
| 27 | +# | |
| 28 | +# Exits non-zero if any row is unreachable, so a partial survey cannot be | |
| 29 | +# mistaken for a clean one. | |
| 30 | + | |
| 31 | +set -uo pipefail | |
| 32 | + | |
| 33 | +ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) | |
| 34 | +CHAINS=${1:-$ROOT/script/weth-chains.tsv} | |
| 35 | + | |
| 36 | +for cmd in cast; do | |
| 37 | + command -v "$cmd" >/dev/null || { | |
| 38 | + echo "$cmd not found — install foundry" >&2 | |
| 39 | + exit 1 | |
| 40 | + } | |
| 41 | +done | |
| 42 | + | |
| 43 | +[[ -f $CHAINS ]] || { | |
| 44 | + echo "no chain table at $CHAINS" >&2 | |
| 45 | + exit 1 | |
| 46 | +} | |
| 47 | + | |
| 48 | +# ERC-20 plus the two wrapper entrypoints. Code missing any of these is not a | |
| 49 | +# wETH this project can peg to, whatever it is called. | |
| 50 | +SELECTORS=( | |
| 51 | + d0e30db0 # deposit() | |
| 52 | + 2e1a7d4d # withdraw(uint256) | |
| 53 | + a9059cbb # transfer(address,uint256) | |
| 54 | + 70a08231 # balanceOf(address) | |
| 55 | + 095ea7b3 # approve(address,uint256) | |
| 56 | + 23b872dd # transferFrom(address,address,uint256) | |
| 57 | +) | |
| 58 | + | |
| 59 | +# Drops solc's metadata blob: the last two bytes hold its length, and the blob | |
| 60 | +# itself starts with a CBOR map (0xa1, 0xa2, …). Anything that fails those two | |
| 61 | +# sanity checks is left whole rather than guessed at. | |
| 62 | +strip_metadata() { | |
| 63 | + local hex=$1 len total marker | |
| 64 | + total=$((${#hex} / 2)) | |
| 65 | + [[ ${#hex} -gt 4 ]] || { | |
| 66 | + printf '%s' "$hex" | |
| 67 | + return | |
| 68 | + } | |
| 69 | + | |
| 70 | + len=$((16#${hex: -4})) | |
| 71 | + if ((len + 2 >= total || len == 0)); then | |
| 72 | + printf '%s' "$hex" | |
| 73 | + return | |
| 74 | + fi | |
| 75 | + | |
| 76 | + marker=${hex:$((${#hex} - (len + 2) * 2)):2} | |
| 77 | + if [[ $marker != a* ]]; then | |
| 78 | + printf '%s' "$hex" | |
| 79 | + return | |
| 80 | + fi | |
| 81 | + | |
| 82 | + printf '%s' "${hex:0:$((${#hex} - (len + 2) * 2))}" | |
| 83 | +} | |
| 84 | + | |
| 85 | +rows=() | |
| 86 | +unreachable=0 | |
| 87 | + | |
| 88 | +printf '%-16s %-9s %-8s %-12s %s\n' CHAIN ID BYTES KIND CODEHASH | |
| 89 | + | |
| 90 | +while IFS=$'\t' read -r chain_id name weth rpc; do | |
| 91 | + [[ -z ${chain_id:-} || $chain_id == \#* ]] && continue | |
| 92 | + | |
| 93 | + code=$(cast code "$weth" --rpc-url "$rpc" 2>/dev/null) | |
| 94 | + | |
| 95 | + if [[ ! $code == 0x* || ${#code} -le 4 ]]; then | |
| 96 | + printf '%-16s %-9s %-8s %-12s %s\n' "$name" "$chain_id" "-" "-" "UNREACHABLE or no code" | |
| 97 | + unreachable=$((unreachable + 1)) | |
| 98 | + continue | |
| 99 | + fi | |
| 100 | + | |
| 101 | + hex=${code#0x} | |
| 102 | + body=$(strip_metadata "$hex") | |
| 103 | + | |
| 104 | + codehash=$(cast keccak "0x$hex") | |
| 105 | + family=$(cast keccak "0x$body") | |
| 106 | + | |
| 107 | + kind=wrapper | |
| 108 | + for selector in "${SELECTORS[@]}"; do | |
| 109 | + [[ $body == *"$selector"* ]] || { | |
| 110 | + kind=not-wETH | |
| 111 | + break | |
| 112 | + } | |
| 113 | + done | |
| 114 | + | |
| 115 | + printf '%-16s %-9s %-8s %-12s %s\n' "$name" "$chain_id" "$((${#hex} / 2))" "$kind" "$codehash" | |
| 116 | + rows+=("$family|$codehash|$name|$kind|$((${#hex} / 2))") | |
| 117 | +done <"$CHAINS" | |
| 118 | + | |
| 119 | +echo | |
| 120 | +echo "families — one block per implementation, indented lines are the allowlist entries" | |
| 121 | +echo | |
| 122 | + | |
| 123 | +wrapper_chains=0 | |
| 124 | +wrapper_hashes=0 | |
| 125 | +wrapper_families=0 | |
| 126 | + | |
| 127 | +for family in $(printf '%s\n' ${rows[@]+"${rows[@]}"} | cut -d'|' -f1 | sort -u); do | |
| 128 | + block=$(printf '%s\n' ${rows[@]+"${rows[@]}"} | grep "^$family|") | |
| 129 | + kinds=$(printf '%s\n' "$block" | cut -d'|' -f4 | sort -u | paste -sd, -) | |
| 130 | + size=$(printf '%s\n' "$block" | head -1 | cut -d'|' -f5) | |
| 131 | + chains=$(printf '%s\n' "$block" | wc -l | tr -d ' ') | |
| 132 | + | |
| 133 | + printf '%s %s bytes %s chain(s) %s\n' "${family:0:18}…" "$size" "$chains" "$kinds" | |
| 134 | + | |
| 135 | + for codehash in $(printf '%s\n' "$block" | cut -d'|' -f2 | sort -u); do | |
| 136 | + names=$(printf '%s\n' "$block" | grep "|$codehash|" | cut -d'|' -f3 | sort | paste -sd' ' -) | |
| 137 | + printf ' %s %s\n' "$codehash" "$names" | |
| 138 | + | |
| 139 | + if [[ $kinds == wrapper ]]; then | |
| 140 | + wrapper_hashes=$((wrapper_hashes + 1)) | |
| 141 | + fi | |
| 142 | + done | |
| 143 | + | |
| 144 | + if [[ $kinds == wrapper ]]; then | |
| 145 | + wrapper_families=$((wrapper_families + 1)) | |
| 146 | + wrapper_chains=$((wrapper_chains + chains)) | |
| 147 | + fi | |
| 148 | + | |
| 149 | + echo | |
| 150 | +done | |
| 151 | + | |
| 152 | +printf '%s chains read, %s unreachable\n' "${#rows[@]}" "$unreachable" | |
| 153 | +printf 'wrappers: %s chains, %s families, %s exact codehashes to allowlist\n' \ | |
| 154 | + "$wrapper_chains" "$wrapper_families" "$wrapper_hashes" | |
| 155 | +echo | |
| 156 | +echo "A wrapper family is a candidate, not an entry — read its source before" | |
| 157 | +echo "adding the hash. Proxies and bridged-ETH tokens show up as not-wETH." | |
| 158 | + | |
| 159 | +[[ $unreachable -eq 0 ]] || { | |
| 160 | + echo >&2 | |
| 161 | + echo "$unreachable row(s) unreachable — survey is incomplete" >&2 | |
| 162 | + exit 1 | |
| 163 | +} | |
| new file mode 100755 | |||
| @@ -0,0 +1,163 @@ | |||
| 1 | +#!/usr/bin/env bash | ||
| 2 | +# | ||
| 3 | +# Surveys the wETH deployment on every chain in weth-chains.tsv and groups them | ||
| 4 | +# by implementation, to answer the question the allowlist in | ||
| 5 | +# src/WETHRegistry.sol depends on: how many distinct wETH implementations are | ||
| 6 | +# actually out there? | ||
| 7 | +# | ||
| 8 | +# For each chain it reports three things: | ||
| 9 | +# | ||
| 10 | +# codehash keccak256 of the runtime code — exactly what EXTCODEHASH returns | ||
| 11 | +# and what the registry compares against, so these values are the | ||
| 12 | +# allowlist entries verbatim. | ||
| 13 | +# family the same hash with solc's trailing metadata blob removed. That | ||
| 14 | +# blob is never executed and embeds a hash of the source path and | ||
| 15 | +# comments, so one implementation compiled twice gets two | ||
| 16 | +# codehashes but one family. Grouping by family is how the exact | ||
| 17 | +# hashes get reviewed together; the registry itself never strips. | ||
| 18 | +# kind whether the code carries the ERC-20 and wrapper entrypoints | ||
| 19 | +# itself. A bridged-ETH token has no deposit/withdraw, and a proxy | ||
| 20 | +# has no entrypoints at all — its codehash commits to a forwarder, | ||
| 21 | +# not to behaviour, so it must never be allowlisted. | ||
| 22 | +# | ||
| 23 | +# Chains are read with eth_getCode and hashed locally rather than with | ||
| 24 | +# eth_getProof, which several public RPCs do not serve. | ||
| 25 | +# | ||
| 26 | +# Usage: ./script/weth-codehashes.sh [chains.tsv] | ||
| 27 | +# | ||
| 28 | +# Exits non-zero if any row is unreachable, so a partial survey cannot be | ||
| 29 | +# mistaken for a clean one. | ||
| 30 | + | ||
| 31 | +set -uo pipefail | ||
| 32 | + | ||
| 33 | +ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) | ||
| 34 | +CHAINS=${1:-$ROOT/script/weth-chains.tsv} | ||
| 35 | + | ||
| 36 | +for cmd in cast; do | ||
| 37 | + command -v "$cmd" >/dev/null || { | ||
| 38 | + echo "$cmd not found — install foundry" >&2 | ||
| 39 | + exit 1 | ||
| 40 | + } | ||
| 41 | +done | ||
| 42 | + | ||
| 43 | +[[ -f $CHAINS ]] || { | ||
| 44 | + echo "no chain table at $CHAINS" >&2 | ||
| 45 | + exit 1 | ||
| 46 | +} | ||
| 47 | + | ||
| 48 | +# ERC-20 plus the two wrapper entrypoints. Code missing any of these is not a | ||
| 49 | +# wETH this project can peg to, whatever it is called. | ||
| 50 | +SELECTORS=( | ||
| 51 | + d0e30db0 # deposit() | ||
| 52 | + 2e1a7d4d # withdraw(uint256) | ||
| 53 | + a9059cbb # transfer(address,uint256) | ||
| 54 | + 70a08231 # balanceOf(address) | ||
| 55 | + 095ea7b3 # approve(address,uint256) | ||
| 56 | + 23b872dd # transferFrom(address,address,uint256) | ||
| 57 | +) | ||
| 58 | + | ||
| 59 | +# Drops solc's metadata blob: the last two bytes hold its length, and the blob | ||
| 60 | +# itself starts with a CBOR map (0xa1, 0xa2, …). Anything that fails those two | ||
| 61 | +# sanity checks is left whole rather than guessed at. | ||
| 62 | +strip_metadata() { | ||
| 63 | + local hex=$1 len total marker | ||
| 64 | + total=$((${#hex} / 2)) | ||
| 65 | + [[ ${#hex} -gt 4 ]] || { | ||
| 66 | + printf '%s' "$hex" | ||
| 67 | + return | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + len=$((16#${hex: -4})) | ||
| 71 | + if ((len + 2 >= total || len == 0)); then | ||
| 72 | + printf '%s' "$hex" | ||
| 73 | + return | ||
| 74 | + fi | ||
| 75 | + | ||
| 76 | + marker=${hex:$((${#hex} - (len + 2) * 2)):2} | ||
| 77 | + if [[ $marker != a* ]]; then | ||
| 78 | + printf '%s' "$hex" | ||
| 79 | + return | ||
| 80 | + fi | ||
| 81 | + | ||
| 82 | + printf '%s' "${hex:0:$((${#hex} - (len + 2) * 2))}" | ||
| 83 | +} | ||
| 84 | + | ||
| 85 | +rows=() | ||
| 86 | +unreachable=0 | ||
| 87 | + | ||
| 88 | +printf '%-16s %-9s %-8s %-12s %s\n' CHAIN ID BYTES KIND CODEHASH | ||
| 89 | + | ||
| 90 | +while IFS=$'\t' read -r chain_id name weth rpc; do | ||
| 91 | + [[ -z ${chain_id:-} || $chain_id == \#* ]] && continue | ||
| 92 | + | ||
| 93 | + code=$(cast code "$weth" --rpc-url "$rpc" 2>/dev/null) | ||
| 94 | + | ||
| 95 | + if [[ ! $code == 0x* || ${#code} -le 4 ]]; then | ||
| 96 | + printf '%-16s %-9s %-8s %-12s %s\n' "$name" "$chain_id" "-" "-" "UNREACHABLE or no code" | ||
| 97 | + unreachable=$((unreachable + 1)) | ||
| 98 | + continue | ||
| 99 | + fi | ||
| 100 | + | ||
| 101 | + hex=${code#0x} | ||
| 102 | + body=$(strip_metadata "$hex") | ||
| 103 | + | ||
| 104 | + codehash=$(cast keccak "0x$hex") | ||
| 105 | + family=$(cast keccak "0x$body") | ||
| 106 | + | ||
| 107 | + kind=wrapper | ||
| 108 | + for selector in "${SELECTORS[@]}"; do | ||
| 109 | + [[ $body == *"$selector"* ]] || { | ||
| 110 | + kind=not-wETH | ||
| 111 | + break | ||
| 112 | + } | ||
| 113 | + done | ||
| 114 | + | ||
| 115 | + printf '%-16s %-9s %-8s %-12s %s\n' "$name" "$chain_id" "$((${#hex} / 2))" "$kind" "$codehash" | ||
| 116 | + rows+=("$family|$codehash|$name|$kind|$((${#hex} / 2))") | ||
| 117 | +done <"$CHAINS" | ||
| 118 | + | ||
| 119 | +echo | ||
| 120 | +echo "families — one block per implementation, indented lines are the allowlist entries" | ||
| 121 | +echo | ||
| 122 | + | ||
| 123 | +wrapper_chains=0 | ||
| 124 | +wrapper_hashes=0 | ||
| 125 | +wrapper_families=0 | ||
| 126 | + | ||
| 127 | +for family in $(printf '%s\n' ${rows[@]+"${rows[@]}"} | cut -d'|' -f1 | sort -u); do | ||
| 128 | + block=$(printf '%s\n' ${rows[@]+"${rows[@]}"} | grep "^$family|") | ||
| 129 | + kinds=$(printf '%s\n' "$block" | cut -d'|' -f4 | sort -u | paste -sd, -) | ||
| 130 | + size=$(printf '%s\n' "$block" | head -1 | cut -d'|' -f5) | ||
| 131 | + chains=$(printf '%s\n' "$block" | wc -l | tr -d ' ') | ||
| 132 | + | ||
| 133 | + printf '%s %s bytes %s chain(s) %s\n' "${family:0:18}…" "$size" "$chains" "$kinds" | ||
| 134 | + | ||
| 135 | + for codehash in $(printf '%s\n' "$block" | cut -d'|' -f2 | sort -u); do | ||
| 136 | + names=$(printf '%s\n' "$block" | grep "|$codehash|" | cut -d'|' -f3 | sort | paste -sd' ' -) | ||
| 137 | + printf ' %s %s\n' "$codehash" "$names" | ||
| 138 | + | ||
| 139 | + if [[ $kinds == wrapper ]]; then | ||
| 140 | + wrapper_hashes=$((wrapper_hashes + 1)) | ||
| 141 | + fi | ||
| 142 | + done | ||
| 143 | + | ||
| 144 | + if [[ $kinds == wrapper ]]; then | ||
| 145 | + wrapper_families=$((wrapper_families + 1)) | ||
| 146 | + wrapper_chains=$((wrapper_chains + chains)) | ||
| 147 | + fi | ||
| 148 | + | ||
| 149 | + echo | ||
| 150 | +done | ||
| 151 | + | ||
| 152 | +printf '%s chains read, %s unreachable\n' "${#rows[@]}" "$unreachable" | ||
| 153 | +printf 'wrappers: %s chains, %s families, %s exact codehashes to allowlist\n' \ | ||
| 154 | + "$wrapper_chains" "$wrapper_families" "$wrapper_hashes" | ||
| 155 | +echo | ||
| 156 | +echo "A wrapper family is a candidate, not an entry — read its source before" | ||
| 157 | +echo "adding the hash. Proxies and bridged-ETH tokens show up as not-wETH." | ||
| 158 | + | ||
| 159 | +[[ $unreachable -eq 0 ]] || { | ||
| 160 | + echo >&2 | ||
| 161 | + echo "$unreachable row(s) unreachable — survey is incomplete" >&2 | ||
| 162 | + exit 1 | ||
| 163 | +} | ||
added
src/IWETH.sol +20 -0 | new file mode 100644 | ||
| @@ -0,0 +1,20 @@ | ||
| 1 | +// SPDX-License-Identifier: MIT | |
| 2 | +pragma solidity ^0.8.30; | |
| 3 | + | |
| 4 | +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | |
| 5 | + | |
| 6 | +/// @title IWETH | |
| 7 | +/// @author Julien Béranger | |
| 8 | +/// @notice The wrapped-ether surface `WETHRegistry` probes: ERC-20, plus the | |
| 9 | +/// two entrypoints that make a wrapper a wrapper. | |
| 10 | +/// @dev Deliberately not the full WETH9 ABI. The registry only needs to move | |
| 11 | +/// one wei in and back out again, and a narrower interface is a narrower | |
| 12 | +/// set of assumptions about what a candidate has to implement. | |
| 13 | +interface IWETH is IERC20 { | |
| 14 | + /// @notice Wrap the ether sent with this call, one for one. | |
| 15 | + function deposit() external payable; | |
| 16 | + | |
| 17 | + /// @notice Unwrap `amount`, burning the wrapper token and returning ether. | |
| 18 | + /// @param amount The wrapped ether to unwrap, in wei. | |
| 19 | + function withdraw(uint256 amount) external; | |
| 20 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,20 @@ | |||
| 1 | +// SPDX-License-Identifier: MIT | ||
| 2 | +pragma solidity ^0.8.30; | ||
| 3 | + | ||
| 4 | +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
| 5 | + | ||
| 6 | +/// @title IWETH | ||
| 7 | +/// @author Julien Béranger | ||
| 8 | +/// @notice The wrapped-ether surface `WETHRegistry` probes: ERC-20, plus the | ||
| 9 | +/// two entrypoints that make a wrapper a wrapper. | ||
| 10 | +/// @dev Deliberately not the full WETH9 ABI. The registry only needs to move | ||
| 11 | +/// one wei in and back out again, and a narrower interface is a narrower | ||
| 12 | +/// set of assumptions about what a candidate has to implement. | ||
| 13 | +interface IWETH is IERC20 { | ||
| 14 | + /// @notice Wrap the ether sent with this call, one for one. | ||
| 15 | + function deposit() external payable; | ||
| 16 | + | ||
| 17 | + /// @notice Unwrap `amount`, burning the wrapper token and returning ether. | ||
| 18 | + /// @param amount The wrapped ether to unwrap, in wei. | ||
| 19 | + function withdraw(uint256 amount) external; | ||
| 20 | +} | ||
modified
src/Love.sol +42 -5 | @@ -1,6 +1,8 @@ | ||
| 1 | 1 | // SPDX-License-Identifier: MIT |
| 2 | 2 | pragma solidity ^0.8.30; |
| 3 | 3 | |
| 4 | +import {IWETH} from "./IWETH.sol"; | |
| 5 | +import {WETHRegistry} from "./WETHRegistry.sol"; | |
| 4 | 6 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; |
| 5 | 7 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
| 6 | 8 | import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; |
| @@ -21,9 +23,23 @@ contract Love is ERC20 { | ||
| 21 | 23 | /// @notice LOVE minted per unit of wETH, and burned per unit released. |
| 22 | 24 | uint256 public constant RATE = 100_000; |
| 23 | 25 | |
| 26 | + /// @notice The registry this token asks which wETH to peg to. | |
| 27 | + /// @dev `WETHRegistry` takes no constructor arguments, so CREATE2 puts it | |
| 28 | + /// at this address on every chain. Asking it at construction time, | |
| 29 | + /// rather than taking wETH as a constructor argument, is what keeps | |
| 30 | + /// this contract's creation code byte-identical everywhere — and so | |
| 31 | + /// what gives LOVE one address on every chain instead of one per wETH | |
| 32 | + /// deployment. | |
| 33 | + /// | |
| 34 | + /// Derived from the registry's creation code, which means it moves if | |
| 35 | + /// `WETHRegistry`, the solc version or the optimizer settings change. | |
| 36 | + /// `LoveRegistryAddressTest` recomputes it and fails if this constant | |
| 37 | + /// has drifted. | |
| 38 | + address public constant REGISTRY = 0x9Cf17430fEdEC487518416D1Cdc849b1eE9CDbA3; | |
| 39 | + | |
| 24 | 40 | /// @notice The wETH this token is pegged to and collateralised with. |
| 25 | - /// @dev Immutable, and part of the creation code — two chains only give this | |
| 26 | - /// contract the same CREATE2 address if they share a wETH address. | |
| 41 | + /// @dev Read once from the registry and immutable thereafter. The registry | |
| 42 | + /// is itself write-once, so nothing can move the token under the peg. | |
| 27 | 43 | IERC20 public immutable WETH; |
| 28 | 44 | |
| 29 | 45 | /// @notice Thrown when a withdrawal amount is not a multiple of `RATE`. |
| @@ -31,6 +47,14 @@ contract Love is ERC20 { | ||
| 31 | 47 | /// @param rate The rate it has to be a multiple of. |
| 32 | 48 | error AmountNotDivisibleByRate(uint256 loveAmount, uint256 rate); |
| 33 | 49 | |
| 50 | + /// @notice Thrown when there is no registry on this chain yet. | |
| 51 | + /// @param registry The address the registry would be at. | |
| 52 | + error RegistryNotDeployed(address registry); | |
| 53 | + | |
| 54 | + /// @notice Thrown when the registry exists but holds no wETH yet. | |
| 55 | + /// @param registry The registry that was asked. | |
| 56 | + error WethNotRegistered(address registry); | |
| 57 | + | |
| 34 | 58 | /// @notice Emitted when wETH is locked and LOVE minted. |
| 35 | 59 | /// @param account The depositor, who pays the wETH and receives the LOVE. |
| 36 | 60 | /// @param wethAmount The wETH pulled in. |
| @@ -43,9 +67,22 @@ contract Love is ERC20 { | ||
| 43 | 67 | /// @param wethAmount The wETH released, `loveAmount / RATE`. |
| 44 | 68 | event Withdraw(address indexed account, uint256 loveAmount, uint256 wethAmount); |
| 45 | 69 | |
| 46 | - /// @param weth_ The wETH to peg to. Set once, never changed. | |
| 47 | - constructor(IERC20 weth_) ERC20("Love", "LOVE") { | |
| 48 | - WETH = weth_; | |
| 70 | + /// @notice Peg to whatever wETH the registry has accepted on this chain. | |
| 71 | + /// @dev Takes no arguments on purpose. Anything passed in here would land | |
| 72 | + /// in the creation code and give the token a different address on | |
| 73 | + /// every chain whose wETH sits elsewhere, which is exactly what this | |
| 74 | + /// design exists to avoid. | |
| 75 | + /// | |
| 76 | + /// Reverts when the registry is missing or empty, so a chain with no | |
| 77 | + /// reviewed wETH gets no half-configured token: deploy the registry | |
| 78 | + /// and register wETH first, then deploy this. | |
| 79 | + constructor() ERC20("Love", "LOVE") { | |
| 80 | + if (REGISTRY.code.length == 0) revert RegistryNotDeployed(REGISTRY); | |
| 81 | + | |
| 82 | + IWETH registered = WETHRegistry(payable(REGISTRY)).weth(); | |
| 83 | + if (address(registered) == address(0)) revert WethNotRegistered(REGISTRY); | |
| 84 | + | |
| 85 | + WETH = IERC20(address(registered)); | |
| 49 | 86 | } |
| 50 | 87 | |
| 51 | 88 | /// @notice Lock `wethAmount` wETH and mint `wethAmount * RATE` LOVE to the caller. |
| @@ -1,6 +1,8 @@ | |||
| 1 | // SPDX-License-Identifier: MIT | 1 | // SPDX-License-Identifier: MIT |
| 2 | pragma solidity ^0.8.30; | 2 | pragma solidity ^0.8.30; |
| 3 | 3 | ||
| 4 | +import {IWETH} from "./IWETH.sol"; | ||
| 5 | +import {WETHRegistry} from "./WETHRegistry.sol"; | ||
| 4 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; | 6 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; |
| 5 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | 7 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
| 6 | import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | 8 | import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; |
| @@ -21,9 +23,23 @@ contract Love is ERC20 { | |||
| 21 | /// @notice LOVE minted per unit of wETH, and burned per unit released. | 23 | /// @notice LOVE minted per unit of wETH, and burned per unit released. |
| 22 | uint256 public constant RATE = 100_000; | 24 | uint256 public constant RATE = 100_000; |
| 23 | 25 | ||
| 26 | + /// @notice The registry this token asks which wETH to peg to. | ||
| 27 | + /// @dev `WETHRegistry` takes no constructor arguments, so CREATE2 puts it | ||
| 28 | + /// at this address on every chain. Asking it at construction time, | ||
| 29 | + /// rather than taking wETH as a constructor argument, is what keeps | ||
| 30 | + /// this contract's creation code byte-identical everywhere — and so | ||
| 31 | + /// what gives LOVE one address on every chain instead of one per wETH | ||
| 32 | + /// deployment. | ||
| 33 | + /// | ||
| 34 | + /// Derived from the registry's creation code, which means it moves if | ||
| 35 | + /// `WETHRegistry`, the solc version or the optimizer settings change. | ||
| 36 | + /// `LoveRegistryAddressTest` recomputes it and fails if this constant | ||
| 37 | + /// has drifted. | ||
| 38 | + address public constant REGISTRY = 0x9Cf17430fEdEC487518416D1Cdc849b1eE9CDbA3; | ||
| 39 | + | ||
| 24 | /// @notice The wETH this token is pegged to and collateralised with. | 40 | /// @notice The wETH this token is pegged to and collateralised with. |
| 25 | - /// @dev Immutable, and part of the creation code — two chains only give this | 41 | + /// @dev Read once from the registry and immutable thereafter. The registry |
| 26 | - /// contract the same CREATE2 address if they share a wETH address. | 42 | + /// is itself write-once, so nothing can move the token under the peg. |
| 27 | IERC20 public immutable WETH; | 43 | IERC20 public immutable WETH; |
| 28 | 44 | ||
| 29 | /// @notice Thrown when a withdrawal amount is not a multiple of `RATE`. | 45 | /// @notice Thrown when a withdrawal amount is not a multiple of `RATE`. |
| @@ -31,6 +47,14 @@ contract Love is ERC20 { | |||
| 31 | /// @param rate The rate it has to be a multiple of. | 47 | /// @param rate The rate it has to be a multiple of. |
| 32 | error AmountNotDivisibleByRate(uint256 loveAmount, uint256 rate); | 48 | error AmountNotDivisibleByRate(uint256 loveAmount, uint256 rate); |
| 33 | 49 | ||
| 50 | + /// @notice Thrown when there is no registry on this chain yet. | ||
| 51 | + /// @param registry The address the registry would be at. | ||
| 52 | + error RegistryNotDeployed(address registry); | ||
| 53 | + | ||
| 54 | + /// @notice Thrown when the registry exists but holds no wETH yet. | ||
| 55 | + /// @param registry The registry that was asked. | ||
| 56 | + error WethNotRegistered(address registry); | ||
| 57 | + | ||
| 34 | /// @notice Emitted when wETH is locked and LOVE minted. | 58 | /// @notice Emitted when wETH is locked and LOVE minted. |
| 35 | /// @param account The depositor, who pays the wETH and receives the LOVE. | 59 | /// @param account The depositor, who pays the wETH and receives the LOVE. |
| 36 | /// @param wethAmount The wETH pulled in. | 60 | /// @param wethAmount The wETH pulled in. |
| @@ -43,9 +67,22 @@ contract Love is ERC20 { | |||
| 43 | /// @param wethAmount The wETH released, `loveAmount / RATE`. | 67 | /// @param wethAmount The wETH released, `loveAmount / RATE`. |
| 44 | event Withdraw(address indexed account, uint256 loveAmount, uint256 wethAmount); | 68 | event Withdraw(address indexed account, uint256 loveAmount, uint256 wethAmount); |
| 45 | 69 | ||
| 46 | - /// @param weth_ The wETH to peg to. Set once, never changed. | 70 | + /// @notice Peg to whatever wETH the registry has accepted on this chain. |
| 47 | - constructor(IERC20 weth_) ERC20("Love", "LOVE") { | 71 | + /// @dev Takes no arguments on purpose. Anything passed in here would land |
| 48 | - WETH = weth_; | 72 | + /// in the creation code and give the token a different address on |
| 73 | + /// every chain whose wETH sits elsewhere, which is exactly what this | ||
| 74 | + /// design exists to avoid. | ||
| 75 | + /// | ||
| 76 | + /// Reverts when the registry is missing or empty, so a chain with no | ||
| 77 | + /// reviewed wETH gets no half-configured token: deploy the registry | ||
| 78 | + /// and register wETH first, then deploy this. | ||
| 79 | + constructor() ERC20("Love", "LOVE") { | ||
| 80 | + if (REGISTRY.code.length == 0) revert RegistryNotDeployed(REGISTRY); | ||
| 81 | + | ||
| 82 | + IWETH registered = WETHRegistry(payable(REGISTRY)).weth(); | ||
| 83 | + if (address(registered) == address(0)) revert WethNotRegistered(REGISTRY); | ||
| 84 | + | ||
| 85 | + WETH = IERC20(address(registered)); | ||
| 49 | } | 86 | } |
| 50 | 87 | ||
| 51 | /// @notice Lock `wethAmount` wETH and mint `wethAmount * RATE` LOVE to the caller. | 88 | /// @notice Lock `wethAmount` wETH and mint `wethAmount * RATE` LOVE to the caller. |
added
src/WETHRegistry.sol +203 -0 | new file mode 100644 | ||
| @@ -0,0 +1,203 @@ | ||
| 1 | +// SPDX-License-Identifier: MIT | |
| 2 | +pragma solidity ^0.8.30; | |
| 3 | + | |
| 4 | +import {IWETH} from "./IWETH.sol"; | |
| 5 | + | |
| 6 | +/// @title WETHRegistry | |
| 7 | +/// @author Julien Béranger | |
| 8 | +/// @notice Names the wETH that `Love` pegs to on this chain, and will only | |
| 9 | +/// accept an address whose code is one of a fixed set of reviewed wETH | |
| 10 | +/// implementations. | |
| 11 | +/// @dev The point is to take the wETH address out of `Love`'s creation code. | |
| 12 | +/// Anyone can call `register`, on any chain, without a factory, an owner | |
| 13 | +/// or a per-chain deployer — the contract decides for itself whether to | |
| 14 | +/// accept the candidate. Because it takes no constructor arguments, its | |
| 15 | +/// own creation code is identical everywhere, so CREATE2 puts it at one | |
| 16 | +/// address on every chain and `Love` can hardcode that address. | |
| 17 | +/// | |
| 18 | +/// Two gates run at registration: | |
| 19 | +/// | |
| 20 | +/// 1. `EXTCODEHASH` against the allowlist below. This is the real | |
| 21 | +/// defence: matching means the candidate is provably one of a handful | |
| 22 | +/// of implementations whose source has been read, not merely something | |
| 23 | +/// that behaves well today. It is what catches a hidden mint, an | |
| 24 | +/// upgrade hook or a backdoor, none of which a behavioural test can | |
| 25 | +/// see. | |
| 26 | +/// 2. A one-wei deposit/withdraw round trip, which catches the plain | |
| 27 | +/// mistake of an address that is not wETH at all, and proves the | |
| 28 | +/// wrapper actually works on this chain. | |
| 29 | +/// | |
| 30 | +/// The registration is write-once. There is no setter, no owner and no | |
| 31 | +/// way to revoke an entry, so a `Love` deployed against this registry can | |
| 32 | +/// never have the token under its peg swapped. | |
| 33 | +/// | |
| 34 | +/// Known limitation, and it is not a small one: the allowlist proves a | |
| 35 | +/// candidate *is* a reviewed wETH implementation, not that it is *the* | |
| 36 | +/// wETH the chain's ecosystem uses. Anyone can deploy their own copy of | |
| 37 | +/// WETH9 — identical code, identical codehash, no liquidity — and | |
| 38 | +/// register it first. The result is still fully backed and redeemable, | |
| 39 | +/// since it is real WETH9, but it is not the token anyone else holds, and | |
| 40 | +/// write-once means the mistake cannot be corrected on that chain. So | |
| 41 | +/// always check `weth()` against the chain's canonical wETH before | |
| 42 | +/// treating a `Love` instance as the real one; a squatted registry is | |
| 43 | +/// visible to anyone who looks, and the answer to it is a fresh salt. | |
| 44 | +contract WETHRegistry { | |
| 45 | + /// @notice The wETH registered on this chain, or the zero address if none | |
| 46 | + /// has been registered yet. | |
| 47 | + /// @dev Written exactly once, by whoever calls `register` first with a | |
| 48 | + /// candidate that passes both gates. | |
| 49 | + IWETH public weth; | |
| 50 | + | |
| 51 | + /// @notice The ether moved through the candidate to prove it wraps. | |
| 52 | + /// @dev Stays in the registry afterwards; see `register`. | |
| 53 | + uint256 public constant PROBE = 1 wei; | |
| 54 | + | |
| 55 | + /// @dev Open only for the duration of the round trip, so the registry | |
| 56 | + /// cannot be used as a place to park ether. Transient, so it costs | |
| 57 | + /// almost nothing and cannot survive the call that set it. | |
| 58 | + bool private transient _probing; | |
| 59 | + | |
| 60 | + /// @notice Thrown when a wETH has already been registered on this chain. | |
| 61 | + /// @param registered The wETH registered by the earlier call. | |
| 62 | + error AlreadyRegistered(IWETH registered); | |
| 63 | + | |
| 64 | + /// @notice Thrown when the candidate's code is not a reviewed wETH. | |
| 65 | + /// @param candidate The rejected address. | |
| 66 | + /// @param codeHash Its `EXTCODEHASH`, zero if there is no code there. | |
| 67 | + error UnknownImplementation(IWETH candidate, bytes32 codeHash); | |
| 68 | + | |
| 69 | + /// @notice Thrown when the call does not carry exactly `PROBE` wei. | |
| 70 | + /// @param sent The value that came with the call. | |
| 71 | + error ProbeValueRequired(uint256 sent); | |
| 72 | + | |
| 73 | + /// @notice Thrown when wrapping `PROBE` wei did not mint `PROBE` wrapped. | |
| 74 | + /// @param expected The balance a real wrapper would have produced. | |
| 75 | + /// @param actual The balance the candidate produced. | |
| 76 | + error DepositMismatch(uint256 expected, uint256 actual); | |
| 77 | + | |
| 78 | + /// @notice Thrown when unwrapping did not burn the wrapped token. | |
| 79 | + /// @param expected The balance a real wrapper would have left behind. | |
| 80 | + /// @param actual The balance the candidate left behind. | |
| 81 | + error WithdrawMismatch(uint256 expected, uint256 actual); | |
| 82 | + | |
| 83 | + /// @notice Thrown when unwrapping did not return the ether. | |
| 84 | + /// @param expected The ether balance the round trip should have restored. | |
| 85 | + /// @param actual The ether balance it actually left. | |
| 86 | + error EtherNotReturned(uint256 expected, uint256 actual); | |
| 87 | + | |
| 88 | + /// @notice Thrown when ether is sent outside a round trip. | |
| 89 | + error NotProbing(); | |
| 90 | + | |
| 91 | + /// @notice Emitted once, when a chain's wETH is settled. | |
| 92 | + /// @param weth The accepted wETH. | |
| 93 | + /// @param registrar Whoever supplied and paid for it. | |
| 94 | + /// @param codeHash The allowlisted hash its code matched. | |
| 95 | + event Registered(IWETH indexed weth, address indexed registrar, bytes32 codeHash); | |
| 96 | + | |
| 97 | + /// @notice Accept `candidate` as this chain's wETH, if its code is one of | |
| 98 | + /// the reviewed implementations and it wraps ether correctly. | |
| 99 | + /// @dev Send exactly `PROBE` wei. It is not refunded: it stays here, which | |
| 100 | + /// is the cheap way to let a contract register. Paying it back would | |
| 101 | + /// mean calling the registrar with value, and a registrar with no | |
| 102 | + /// payable fallback — a script, a multisig, a deployer contract — | |
| 103 | + /// would then be unable to register at all. One wei, once per chain, | |
| 104 | + /// buys that away. | |
| 105 | + /// | |
| 106 | + /// Reverts rather than degrading when the candidate is unrecognised — | |
| 107 | + /// unreviewed bytecode cannot be shown safe by any test, static or | |
| 108 | + /// behavioural, so a chain running its own wETH is a chain `Love` | |
| 109 | + /// does not deploy on until that implementation is reviewed and | |
| 110 | + /// added. | |
| 111 | + /// @param candidate The wETH to register. | |
| 112 | + /// @return The registered wETH, for the convenience of scripts. | |
| 113 | + function register(IWETH candidate) external payable returns (IWETH) { | |
| 114 | + if (address(weth) != address(0)) revert AlreadyRegistered(weth); | |
| 115 | + if (msg.value != PROBE) revert ProbeValueRequired(msg.value); | |
| 116 | + | |
| 117 | + bytes32 codeHash = address(candidate).codehash; | |
| 118 | + if (!isKnownImplementation(codeHash)) revert UnknownImplementation(candidate, codeHash); | |
| 119 | + | |
| 120 | + _probe(candidate); | |
| 121 | + | |
| 122 | + weth = candidate; | |
| 123 | + emit Registered(candidate, msg.sender, codeHash); | |
| 124 | + | |
| 125 | + return candidate; | |
| 126 | + } | |
| 127 | + | |
| 128 | + /// @notice Whether `codeHash` is one of the reviewed wETH implementations. | |
| 129 | + /// @dev Compile-time, so the list is ownerless and append-only by | |
| 130 | + /// construction: extending it means publishing a new registry, which | |
| 131 | + /// leaves every existing deployment exactly as it was. No key can | |
| 132 | + /// revoke an entry and strand a live `Love`. | |
| 133 | + /// | |
| 134 | + /// These are exact `EXTCODEHASH` values, metadata included. Hashing | |
| 135 | + /// the code with solc's trailing metadata stripped would fold each | |
| 136 | + /// family into a single entry, but it would also accept a known | |
| 137 | + /// implementation followed by arbitrary appended bytes, and the | |
| 138 | + /// safety of that rests on control flow never reaching them — a | |
| 139 | + /// property that holds for every family here and would have to keep | |
| 140 | + /// holding for every family added later. Ten constants is the cheaper | |
| 141 | + /// side of that trade. | |
| 142 | + /// | |
| 143 | + /// Grouped by implementation. Every value is reproducible from chain | |
| 144 | + /// state with `script/weth-codehashes.sh`. | |
| 145 | + /// @param codeHash The `EXTCODEHASH` to check. | |
| 146 | + /// @return True if a candidate with this code may be registered. | |
| 147 | + function isKnownImplementation(bytes32 codeHash) public pure returns (bool) { | |
| 148 | + // OP Stack legacy WETH9, 2041 bytes, solc 0.5.17. One implementation, | |
| 149 | + // four hashes: these chains run byte-identical code and differ only | |
| 150 | + // inside solc's metadata blob, which never executes. | |
| 151 | + if (codeHash == 0x779bbf2a738ef09d961c945116197e2ac764c1b39304b2b4418cd4e42668b173) return true; // optimism | |
| 152 | + if (codeHash == 0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c) return true; // base, mode, zora | |
| 153 | + if (codeHash == 0x557c8e14d33f7cd67cad0141e1a49ebf3488a447fc3df7aa66b127778a0383d1) return true; // world-chain | |
| 154 | + if (codeHash == 0xf35fe602ba2a3b96f2e27ff7c8b8010800a8d0d616a5fb1f902e087b590355f3) return true; // lisk | |
| 155 | + | |
| 156 | + // Canonical WETH9, 3124 bytes, solc 0.4.19. | |
| 157 | + if (codeHash == 0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23) return true; // ethereum | |
| 158 | + if (codeHash == 0xa670ec6c272ddec6d328d6f3d5cad65a841a6ab45e8e5cf825150eb458be4f1f) return true; // linea | |
| 159 | + if (codeHash == 0x032e9cab14331328530468e54f1b91777b4d5c9dbbb400884badb32bc4113585) return true; // polygon-zkevm | |
| 160 | + | |
| 161 | + // OP Stack WETH, 2865 bytes, solc 0.8.15. | |
| 162 | + if (codeHash == 0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012) return true; // unichain, soneium, ink | |
| 163 | + | |
| 164 | + // Taiko, 3204 bytes. | |
| 165 | + if (codeHash == 0x9f3d95086909fce850d997158aba31abe26c3aad6a413107ca0bf9d53a7c42e9) return true; // taiko | |
| 166 | + | |
| 167 | + // Scroll, 5871 bytes. | |
| 168 | + if (codeHash == 0xe8c4073351c26b9831c1e5af153b9be4713a4af9edfdf32b58077b735e120f14) return true; // scroll | |
| 169 | + | |
| 170 | + return false; | |
| 171 | + } | |
| 172 | + | |
| 173 | + /// @notice Take `PROBE` wei through the candidate and back. | |
| 174 | + /// @dev Costs the registrar nothing but gas: the wei returns. Balances are | |
| 175 | + /// read before and after rather than assumed to start at zero, so a | |
| 176 | + /// candidate that was sent wETH beforehand cannot skew the check. | |
| 177 | + /// @param candidate The wETH being probed. | |
| 178 | + function _probe(IWETH candidate) private { | |
| 179 | + uint256 etherBefore = address(this).balance; | |
| 180 | + uint256 wrappedBefore = candidate.balanceOf(address(this)); | |
| 181 | + | |
| 182 | + _probing = true; | |
| 183 | + | |
| 184 | + candidate.deposit{value: PROBE}(); | |
| 185 | + uint256 wrapped = candidate.balanceOf(address(this)); | |
| 186 | + if (wrapped != wrappedBefore + PROBE) revert DepositMismatch(wrappedBefore + PROBE, wrapped); | |
| 187 | + | |
| 188 | + candidate.withdraw(PROBE); | |
| 189 | + uint256 unwrapped = candidate.balanceOf(address(this)); | |
| 190 | + if (unwrapped != wrappedBefore) revert WithdrawMismatch(wrappedBefore, unwrapped); | |
| 191 | + | |
| 192 | + _probing = false; | |
| 193 | + | |
| 194 | + if (address(this).balance != etherBefore) revert EtherNotReturned(etherBefore, address(this).balance); | |
| 195 | + } | |
| 196 | + | |
| 197 | + /// @notice Takes the ether a candidate returns mid-probe, and nothing else. | |
| 198 | + /// @dev The registry is not a wallet. Outside a round trip this reverts, | |
| 199 | + /// so ether cannot be stranded in a contract with no way to move it. | |
| 200 | + receive() external payable { | |
| 201 | + if (!_probing) revert NotProbing(); | |
| 202 | + } | |
| 203 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,203 @@ | |||
| 1 | +// SPDX-License-Identifier: MIT | ||
| 2 | +pragma solidity ^0.8.30; | ||
| 3 | + | ||
| 4 | +import {IWETH} from "./IWETH.sol"; | ||
| 5 | + | ||
| 6 | +/// @title WETHRegistry | ||
| 7 | +/// @author Julien Béranger | ||
| 8 | +/// @notice Names the wETH that `Love` pegs to on this chain, and will only | ||
| 9 | +/// accept an address whose code is one of a fixed set of reviewed wETH | ||
| 10 | +/// implementations. | ||
| 11 | +/// @dev The point is to take the wETH address out of `Love`'s creation code. | ||
| 12 | +/// Anyone can call `register`, on any chain, without a factory, an owner | ||
| 13 | +/// or a per-chain deployer — the contract decides for itself whether to | ||
| 14 | +/// accept the candidate. Because it takes no constructor arguments, its | ||
| 15 | +/// own creation code is identical everywhere, so CREATE2 puts it at one | ||
| 16 | +/// address on every chain and `Love` can hardcode that address. | ||
| 17 | +/// | ||
| 18 | +/// Two gates run at registration: | ||
| 19 | +/// | ||
| 20 | +/// 1. `EXTCODEHASH` against the allowlist below. This is the real | ||
| 21 | +/// defence: matching means the candidate is provably one of a handful | ||
| 22 | +/// of implementations whose source has been read, not merely something | ||
| 23 | +/// that behaves well today. It is what catches a hidden mint, an | ||
| 24 | +/// upgrade hook or a backdoor, none of which a behavioural test can | ||
| 25 | +/// see. | ||
| 26 | +/// 2. A one-wei deposit/withdraw round trip, which catches the plain | ||
| 27 | +/// mistake of an address that is not wETH at all, and proves the | ||
| 28 | +/// wrapper actually works on this chain. | ||
| 29 | +/// | ||
| 30 | +/// The registration is write-once. There is no setter, no owner and no | ||
| 31 | +/// way to revoke an entry, so a `Love` deployed against this registry can | ||
| 32 | +/// never have the token under its peg swapped. | ||
| 33 | +/// | ||
| 34 | +/// Known limitation, and it is not a small one: the allowlist proves a | ||
| 35 | +/// candidate *is* a reviewed wETH implementation, not that it is *the* | ||
| 36 | +/// wETH the chain's ecosystem uses. Anyone can deploy their own copy of | ||
| 37 | +/// WETH9 — identical code, identical codehash, no liquidity — and | ||
| 38 | +/// register it first. The result is still fully backed and redeemable, | ||
| 39 | +/// since it is real WETH9, but it is not the token anyone else holds, and | ||
| 40 | +/// write-once means the mistake cannot be corrected on that chain. So | ||
| 41 | +/// always check `weth()` against the chain's canonical wETH before | ||
| 42 | +/// treating a `Love` instance as the real one; a squatted registry is | ||
| 43 | +/// visible to anyone who looks, and the answer to it is a fresh salt. | ||
| 44 | +contract WETHRegistry { | ||
| 45 | + /// @notice The wETH registered on this chain, or the zero address if none | ||
| 46 | + /// has been registered yet. | ||
| 47 | + /// @dev Written exactly once, by whoever calls `register` first with a | ||
| 48 | + /// candidate that passes both gates. | ||
| 49 | + IWETH public weth; | ||
| 50 | + | ||
| 51 | + /// @notice The ether moved through the candidate to prove it wraps. | ||
| 52 | + /// @dev Stays in the registry afterwards; see `register`. | ||
| 53 | + uint256 public constant PROBE = 1 wei; | ||
| 54 | + | ||
| 55 | + /// @dev Open only for the duration of the round trip, so the registry | ||
| 56 | + /// cannot be used as a place to park ether. Transient, so it costs | ||
| 57 | + /// almost nothing and cannot survive the call that set it. | ||
| 58 | + bool private transient _probing; | ||
| 59 | + | ||
| 60 | + /// @notice Thrown when a wETH has already been registered on this chain. | ||
| 61 | + /// @param registered The wETH registered by the earlier call. | ||
| 62 | + error AlreadyRegistered(IWETH registered); | ||
| 63 | + | ||
| 64 | + /// @notice Thrown when the candidate's code is not a reviewed wETH. | ||
| 65 | + /// @param candidate The rejected address. | ||
| 66 | + /// @param codeHash Its `EXTCODEHASH`, zero if there is no code there. | ||
| 67 | + error UnknownImplementation(IWETH candidate, bytes32 codeHash); | ||
| 68 | + | ||
| 69 | + /// @notice Thrown when the call does not carry exactly `PROBE` wei. | ||
| 70 | + /// @param sent The value that came with the call. | ||
| 71 | + error ProbeValueRequired(uint256 sent); | ||
| 72 | + | ||
| 73 | + /// @notice Thrown when wrapping `PROBE` wei did not mint `PROBE` wrapped. | ||
| 74 | + /// @param expected The balance a real wrapper would have produced. | ||
| 75 | + /// @param actual The balance the candidate produced. | ||
| 76 | + error DepositMismatch(uint256 expected, uint256 actual); | ||
| 77 | + | ||
| 78 | + /// @notice Thrown when unwrapping did not burn the wrapped token. | ||
| 79 | + /// @param expected The balance a real wrapper would have left behind. | ||
| 80 | + /// @param actual The balance the candidate left behind. | ||
| 81 | + error WithdrawMismatch(uint256 expected, uint256 actual); | ||
| 82 | + | ||
| 83 | + /// @notice Thrown when unwrapping did not return the ether. | ||
| 84 | + /// @param expected The ether balance the round trip should have restored. | ||
| 85 | + /// @param actual The ether balance it actually left. | ||
| 86 | + error EtherNotReturned(uint256 expected, uint256 actual); | ||
| 87 | + | ||
| 88 | + /// @notice Thrown when ether is sent outside a round trip. | ||
| 89 | + error NotProbing(); | ||
| 90 | + | ||
| 91 | + /// @notice Emitted once, when a chain's wETH is settled. | ||
| 92 | + /// @param weth The accepted wETH. | ||
| 93 | + /// @param registrar Whoever supplied and paid for it. | ||
| 94 | + /// @param codeHash The allowlisted hash its code matched. | ||
| 95 | + event Registered(IWETH indexed weth, address indexed registrar, bytes32 codeHash); | ||
| 96 | + | ||
| 97 | + /// @notice Accept `candidate` as this chain's wETH, if its code is one of | ||
| 98 | + /// the reviewed implementations and it wraps ether correctly. | ||
| 99 | + /// @dev Send exactly `PROBE` wei. It is not refunded: it stays here, which | ||
| 100 | + /// is the cheap way to let a contract register. Paying it back would | ||
| 101 | + /// mean calling the registrar with value, and a registrar with no | ||
| 102 | + /// payable fallback — a script, a multisig, a deployer contract — | ||
| 103 | + /// would then be unable to register at all. One wei, once per chain, | ||
| 104 | + /// buys that away. | ||
| 105 | + /// | ||
| 106 | + /// Reverts rather than degrading when the candidate is unrecognised — | ||
| 107 | + /// unreviewed bytecode cannot be shown safe by any test, static or | ||
| 108 | + /// behavioural, so a chain running its own wETH is a chain `Love` | ||
| 109 | + /// does not deploy on until that implementation is reviewed and | ||
| 110 | + /// added. | ||
| 111 | + /// @param candidate The wETH to register. | ||
| 112 | + /// @return The registered wETH, for the convenience of scripts. | ||
| 113 | + function register(IWETH candidate) external payable returns (IWETH) { | ||
| 114 | + if (address(weth) != address(0)) revert AlreadyRegistered(weth); | ||
| 115 | + if (msg.value != PROBE) revert ProbeValueRequired(msg.value); | ||
| 116 | + | ||
| 117 | + bytes32 codeHash = address(candidate).codehash; | ||
| 118 | + if (!isKnownImplementation(codeHash)) revert UnknownImplementation(candidate, codeHash); | ||
| 119 | + | ||
| 120 | + _probe(candidate); | ||
| 121 | + | ||
| 122 | + weth = candidate; | ||
| 123 | + emit Registered(candidate, msg.sender, codeHash); | ||
| 124 | + | ||
| 125 | + return candidate; | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | + /// @notice Whether `codeHash` is one of the reviewed wETH implementations. | ||
| 129 | + /// @dev Compile-time, so the list is ownerless and append-only by | ||
| 130 | + /// construction: extending it means publishing a new registry, which | ||
| 131 | + /// leaves every existing deployment exactly as it was. No key can | ||
| 132 | + /// revoke an entry and strand a live `Love`. | ||
| 133 | + /// | ||
| 134 | + /// These are exact `EXTCODEHASH` values, metadata included. Hashing | ||
| 135 | + /// the code with solc's trailing metadata stripped would fold each | ||
| 136 | + /// family into a single entry, but it would also accept a known | ||
| 137 | + /// implementation followed by arbitrary appended bytes, and the | ||
| 138 | + /// safety of that rests on control flow never reaching them — a | ||
| 139 | + /// property that holds for every family here and would have to keep | ||
| 140 | + /// holding for every family added later. Ten constants is the cheaper | ||
| 141 | + /// side of that trade. | ||
| 142 | + /// | ||
| 143 | + /// Grouped by implementation. Every value is reproducible from chain | ||
| 144 | + /// state with `script/weth-codehashes.sh`. | ||
| 145 | + /// @param codeHash The `EXTCODEHASH` to check. | ||
| 146 | + /// @return True if a candidate with this code may be registered. | ||
| 147 | + function isKnownImplementation(bytes32 codeHash) public pure returns (bool) { | ||
| 148 | + // OP Stack legacy WETH9, 2041 bytes, solc 0.5.17. One implementation, | ||
| 149 | + // four hashes: these chains run byte-identical code and differ only | ||
| 150 | + // inside solc's metadata blob, which never executes. | ||
| 151 | + if (codeHash == 0x779bbf2a738ef09d961c945116197e2ac764c1b39304b2b4418cd4e42668b173) return true; // optimism | ||
| 152 | + if (codeHash == 0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c) return true; // base, mode, zora | ||
| 153 | + if (codeHash == 0x557c8e14d33f7cd67cad0141e1a49ebf3488a447fc3df7aa66b127778a0383d1) return true; // world-chain | ||
| 154 | + if (codeHash == 0xf35fe602ba2a3b96f2e27ff7c8b8010800a8d0d616a5fb1f902e087b590355f3) return true; // lisk | ||
| 155 | + | ||
| 156 | + // Canonical WETH9, 3124 bytes, solc 0.4.19. | ||
| 157 | + if (codeHash == 0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23) return true; // ethereum | ||
| 158 | + if (codeHash == 0xa670ec6c272ddec6d328d6f3d5cad65a841a6ab45e8e5cf825150eb458be4f1f) return true; // linea | ||
| 159 | + if (codeHash == 0x032e9cab14331328530468e54f1b91777b4d5c9dbbb400884badb32bc4113585) return true; // polygon-zkevm | ||
| 160 | + | ||
| 161 | + // OP Stack WETH, 2865 bytes, solc 0.8.15. | ||
| 162 | + if (codeHash == 0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012) return true; // unichain, soneium, ink | ||
| 163 | + | ||
| 164 | + // Taiko, 3204 bytes. | ||
| 165 | + if (codeHash == 0x9f3d95086909fce850d997158aba31abe26c3aad6a413107ca0bf9d53a7c42e9) return true; // taiko | ||
| 166 | + | ||
| 167 | + // Scroll, 5871 bytes. | ||
| 168 | + if (codeHash == 0xe8c4073351c26b9831c1e5af153b9be4713a4af9edfdf32b58077b735e120f14) return true; // scroll | ||
| 169 | + | ||
| 170 | + return false; | ||
| 171 | + } | ||
| 172 | + | ||
| 173 | + /// @notice Take `PROBE` wei through the candidate and back. | ||
| 174 | + /// @dev Costs the registrar nothing but gas: the wei returns. Balances are | ||
| 175 | + /// read before and after rather than assumed to start at zero, so a | ||
| 176 | + /// candidate that was sent wETH beforehand cannot skew the check. | ||
| 177 | + /// @param candidate The wETH being probed. | ||
| 178 | + function _probe(IWETH candidate) private { | ||
| 179 | + uint256 etherBefore = address(this).balance; | ||
| 180 | + uint256 wrappedBefore = candidate.balanceOf(address(this)); | ||
| 181 | + | ||
| 182 | + _probing = true; | ||
| 183 | + | ||
| 184 | + candidate.deposit{value: PROBE}(); | ||
| 185 | + uint256 wrapped = candidate.balanceOf(address(this)); | ||
| 186 | + if (wrapped != wrappedBefore + PROBE) revert DepositMismatch(wrappedBefore + PROBE, wrapped); | ||
| 187 | + | ||
| 188 | + candidate.withdraw(PROBE); | ||
| 189 | + uint256 unwrapped = candidate.balanceOf(address(this)); | ||
| 190 | + if (unwrapped != wrappedBefore) revert WithdrawMismatch(wrappedBefore, unwrapped); | ||
| 191 | + | ||
| 192 | + _probing = false; | ||
| 193 | + | ||
| 194 | + if (address(this).balance != etherBefore) revert EtherNotReturned(etherBefore, address(this).balance); | ||
| 195 | + } | ||
| 196 | + | ||
| 197 | + /// @notice Takes the ether a candidate returns mid-probe, and nothing else. | ||
| 198 | + /// @dev The registry is not a wallet. Outside a round trip this reverts, | ||
| 199 | + /// so ether cannot be stranded in a contract with no way to move it. | ||
| 200 | + receive() external payable { | ||
| 201 | + if (!_probing) revert NotProbing(); | ||
| 202 | + } | ||
| 203 | +} | ||
added
test/Fixtures.sol +78 -0 | new file mode 100644 | ||
| @@ -0,0 +1,78 @@ | ||
| 1 | +// SPDX-License-Identifier: MIT | |
| 2 | +pragma solidity ^0.8.30; | |
| 3 | + | |
| 4 | +import {IWETH} from "../src/IWETH.sol"; | |
| 5 | +import {WETHRegistry} from "../src/WETHRegistry.sol"; | |
| 6 | +import {CommonBase} from "forge-std/Base.sol"; | |
| 7 | +import {StdCheats} from "forge-std/StdCheats.sol"; | |
| 8 | + | |
| 9 | +/// @title Fixtures | |
| 10 | +/// @author Julien Béranger | |
| 11 | +/// @notice Brings a chain up the way a real one comes up: the registry at its | |
| 12 | +/// deterministic address, a genuine wETH at some address, registered. | |
| 13 | +/// @dev The wETH here is real runtime bytecode lifted off a live chain, etched | |
| 14 | +/// into place. A mock would need its codehash added to the allowlist to | |
| 15 | +/// be registrable, which would mean testing a different allowlist from | |
| 16 | +/// the one that ships. Etching real code keeps the test and the product | |
| 17 | +/// honest about the same ten hashes. | |
| 18 | +abstract contract Fixtures is CommonBase, StdCheats { | |
| 19 | + /// @notice Salt the registry is deployed with, here and in the script. | |
| 20 | + bytes32 internal constant REGISTRY_SALT = keccak256("LOVE.WETHRegistry"); | |
| 21 | + | |
| 22 | + /// @notice OP Stack legacy WETH9, as deployed on Base, Mode and Zora. | |
| 23 | + string internal constant WETH9_OP_LEGACY = "test/fixtures/weth9-op-legacy.hex"; | |
| 24 | + | |
| 25 | + /// @notice Canonical WETH9, as deployed on Ethereum mainnet. | |
| 26 | + string internal constant WETH9_CANONICAL = "test/fixtures/weth9-canonical.hex"; | |
| 27 | + | |
| 28 | + /// @notice Put real wETH runtime bytecode at `where`. | |
| 29 | + /// @dev Reads the fixture rather than embedding it, so refreshing a | |
| 30 | + /// fixture from chain state does not mean editing Solidity. | |
| 31 | + /// @param where The address to place it at. | |
| 32 | + /// @param fixture Path to the runtime bytecode, `WETH9_*` above. | |
| 33 | + /// @return The wETH now living at `where`. | |
| 34 | + function etchWeth(address where, string memory fixture) internal returns (IWETH) { | |
| 35 | + vm.etch(where, vm.parseBytes(vm.trim(vm.readFile(fixture)))); | |
| 36 | + vm.label(where, "WETH"); | |
| 37 | + return IWETH(where); | |
| 38 | + } | |
| 39 | + | |
| 40 | + /// @notice Deploy the registry to the address `Love` expects. | |
| 41 | + /// @dev Through the canonical deterministic deployer with `REGISTRY_SALT`, | |
| 42 | + /// exactly as the deploy script does, so the address it lands at is | |
| 43 | + /// the one compiled into `Love.REGISTRY` rather than one arranged for | |
| 44 | + /// the test. | |
| 45 | + /// @return The registry. | |
| 46 | + function deployRegistry() internal returns (WETHRegistry) { | |
| 47 | + (bool ok, bytes memory ret) = | |
| 48 | + CREATE2_FACTORY.call(abi.encodePacked(REGISTRY_SALT, type(WETHRegistry).creationCode)); | |
| 49 | + require(ok, "registry deployment failed"); | |
| 50 | + | |
| 51 | + // casting to 'bytes20' is safe because the deployer returns the 20-byte address, raw | |
| 52 | + // forge-lint: disable-next-line(unsafe-typecast) | |
| 53 | + WETHRegistry registry = WETHRegistry(payable(address(bytes20(ret)))); | |
| 54 | + vm.label(address(registry), "WETHRegistry"); | |
| 55 | + | |
| 56 | + return registry; | |
| 57 | + } | |
| 58 | + | |
| 59 | + /// @notice Register `weth`, funding the one-wei probe. | |
| 60 | + /// @param registry The registry to register with. | |
| 61 | + /// @param weth The wETH to register. | |
| 62 | + function register(WETHRegistry registry, IWETH weth) internal { | |
| 63 | + uint256 probe = registry.PROBE(); | |
| 64 | + deal(address(this), address(this).balance + probe); | |
| 65 | + registry.register{value: probe}(weth); | |
| 66 | + } | |
| 67 | + | |
| 68 | + /// @notice The whole chain in one call: registry, wETH, registration. | |
| 69 | + /// @param wethAt The address to etch wETH at. | |
| 70 | + /// @param fixture Which implementation to etch. | |
| 71 | + /// @return registry The registry, with `wethAt` registered. | |
| 72 | + /// @return weth The wETH now backing any `Love` deployed here. | |
| 73 | + function setUpChain(address wethAt, string memory fixture) internal returns (WETHRegistry registry, IWETH weth) { | |
| 74 | + registry = deployRegistry(); | |
| 75 | + weth = etchWeth(wethAt, fixture); | |
| 76 | + register(registry, weth); | |
| 77 | + } | |
| 78 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,78 @@ | |||
| 1 | +// SPDX-License-Identifier: MIT | ||
| 2 | +pragma solidity ^0.8.30; | ||
| 3 | + | ||
| 4 | +import {IWETH} from "../src/IWETH.sol"; | ||
| 5 | +import {WETHRegistry} from "../src/WETHRegistry.sol"; | ||
| 6 | +import {CommonBase} from "forge-std/Base.sol"; | ||
| 7 | +import {StdCheats} from "forge-std/StdCheats.sol"; | ||
| 8 | + | ||
| 9 | +/// @title Fixtures | ||
| 10 | +/// @author Julien Béranger | ||
| 11 | +/// @notice Brings a chain up the way a real one comes up: the registry at its | ||
| 12 | +/// deterministic address, a genuine wETH at some address, registered. | ||
| 13 | +/// @dev The wETH here is real runtime bytecode lifted off a live chain, etched | ||
| 14 | +/// into place. A mock would need its codehash added to the allowlist to | ||
| 15 | +/// be registrable, which would mean testing a different allowlist from | ||
| 16 | +/// the one that ships. Etching real code keeps the test and the product | ||
| 17 | +/// honest about the same ten hashes. | ||
| 18 | +abstract contract Fixtures is CommonBase, StdCheats { | ||
| 19 | + /// @notice Salt the registry is deployed with, here and in the script. | ||
| 20 | + bytes32 internal constant REGISTRY_SALT = keccak256("LOVE.WETHRegistry"); | ||
| 21 | + | ||
| 22 | + /// @notice OP Stack legacy WETH9, as deployed on Base, Mode and Zora. | ||
| 23 | + string internal constant WETH9_OP_LEGACY = "test/fixtures/weth9-op-legacy.hex"; | ||
| 24 | + | ||
| 25 | + /// @notice Canonical WETH9, as deployed on Ethereum mainnet. | ||
| 26 | + string internal constant WETH9_CANONICAL = "test/fixtures/weth9-canonical.hex"; | ||
| 27 | + | ||
| 28 | + /// @notice Put real wETH runtime bytecode at `where`. | ||
| 29 | + /// @dev Reads the fixture rather than embedding it, so refreshing a | ||
| 30 | + /// fixture from chain state does not mean editing Solidity. | ||
| 31 | + /// @param where The address to place it at. | ||
| 32 | + /// @param fixture Path to the runtime bytecode, `WETH9_*` above. | ||
| 33 | + /// @return The wETH now living at `where`. | ||
| 34 | + function etchWeth(address where, string memory fixture) internal returns (IWETH) { | ||
| 35 | + vm.etch(where, vm.parseBytes(vm.trim(vm.readFile(fixture)))); | ||
| 36 | + vm.label(where, "WETH"); | ||
| 37 | + return IWETH(where); | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + /// @notice Deploy the registry to the address `Love` expects. | ||
| 41 | + /// @dev Through the canonical deterministic deployer with `REGISTRY_SALT`, | ||
| 42 | + /// exactly as the deploy script does, so the address it lands at is | ||
| 43 | + /// the one compiled into `Love.REGISTRY` rather than one arranged for | ||
| 44 | + /// the test. | ||
| 45 | + /// @return The registry. | ||
| 46 | + function deployRegistry() internal returns (WETHRegistry) { | ||
| 47 | + (bool ok, bytes memory ret) = | ||
| 48 | + CREATE2_FACTORY.call(abi.encodePacked(REGISTRY_SALT, type(WETHRegistry).creationCode)); | ||
| 49 | + require(ok, "registry deployment failed"); | ||
| 50 | + | ||
| 51 | + // casting to 'bytes20' is safe because the deployer returns the 20-byte address, raw | ||
| 52 | + // forge-lint: disable-next-line(unsafe-typecast) | ||
| 53 | + WETHRegistry registry = WETHRegistry(payable(address(bytes20(ret)))); | ||
| 54 | + vm.label(address(registry), "WETHRegistry"); | ||
| 55 | + | ||
| 56 | + return registry; | ||
| 57 | + } | ||
| 58 | + | ||
| 59 | + /// @notice Register `weth`, funding the one-wei probe. | ||
| 60 | + /// @param registry The registry to register with. | ||
| 61 | + /// @param weth The wETH to register. | ||
| 62 | + function register(WETHRegistry registry, IWETH weth) internal { | ||
| 63 | + uint256 probe = registry.PROBE(); | ||
| 64 | + deal(address(this), address(this).balance + probe); | ||
| 65 | + registry.register{value: probe}(weth); | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + /// @notice The whole chain in one call: registry, wETH, registration. | ||
| 69 | + /// @param wethAt The address to etch wETH at. | ||
| 70 | + /// @param fixture Which implementation to etch. | ||
| 71 | + /// @return registry The registry, with `wethAt` registered. | ||
| 72 | + /// @return weth The wETH now backing any `Love` deployed here. | ||
| 73 | + function setUpChain(address wethAt, string memory fixture) internal returns (WETHRegistry registry, IWETH weth) { | ||
| 74 | + registry = deployRegistry(); | ||
| 75 | + weth = etchWeth(wethAt, fixture); | ||
| 76 | + register(registry, weth); | ||
| 77 | + } | ||
| 78 | +} | ||
modified
test/Love.t.sol +18 -13 | @@ -1,22 +1,23 @@ | ||
| 1 | 1 | // SPDX-License-Identifier: MIT |
| 2 | 2 | pragma solidity ^0.8.30; |
| 3 | 3 | |
| 4 | +import {IWETH} from "../src/IWETH.sol"; | |
| 4 | 5 | import {Love} from "../src/Love.sol"; |
| 5 | -import {MockWETH} from "./mocks/MockWETH.sol"; | |
| 6 | +import {Fixtures} from "./Fixtures.sol"; | |
| 6 | 7 | import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; |
| 7 | 8 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
| 8 | 9 | import {Test} from "forge-std/Test.sol"; |
| 9 | 10 | |
| 10 | 11 | /// @title LoveTest |
| 11 | -/// @notice Exercises the peg end to end against a WETH9 stand-in: minting on | |
| 12 | +/// @notice Exercises the peg end to end against real WETH9 bytecode: minting on | |
| 12 | 13 | /// deposit, redemption on withdraw, the divisibility rule, and the |
| 13 | 14 | /// invariant that supply always equals the wETH backing times the rate. |
| 14 | -contract LoveTest is Test { | |
| 15 | +contract LoveTest is Test, Fixtures { | |
| 15 | 16 | /// @notice The token under test. |
| 16 | 17 | Love public love; |
| 17 | 18 | |
| 18 | - /// @notice The wETH it is pegged to. | |
| 19 | - MockWETH public weth; | |
| 19 | + /// @notice The wETH it is pegged to, etched from a live chain. | |
| 20 | + IWETH public weth; | |
| 20 | 21 | |
| 21 | 22 | address alice = makeAddr("alice"); |
| 22 | 23 | address bob = makeAddr("bob"); |
| @@ -24,9 +25,12 @@ contract LoveTest is Test { | ||
| 24 | 25 | |
| 25 | 26 | uint256 constant RATE = 100_000; |
| 26 | 27 | |
| 28 | + /// @dev The OP Stack predeploy address, so the setup mirrors a real chain. | |
| 29 | + address constant WETH_AT = 0x4200000000000000000000000000000000000006; | |
| 30 | + | |
| 27 | 31 | function setUp() public { |
| 28 | - weth = new MockWETH(); | |
| 29 | - love = new Love(IERC20(address(weth))); | |
| 32 | + (, weth) = setUpChain(WETH_AT, WETH9_OP_LEGACY); | |
| 33 | + love = new Love(); | |
| 30 | 34 | |
| 31 | 35 | _fund(alice, 10 ether); |
| 32 | 36 | _fund(bob, 10 ether); |
| @@ -115,11 +119,14 @@ contract LoveTest is Test { | ||
| 115 | 119 | assertEq(love.totalSupply(), 0); |
| 116 | 120 | } |
| 117 | 121 | |
| 122 | + /// @dev WETH9 predates custom errors and guards `transferFrom` with a bare | |
| 123 | + /// `require`, so a shortfall comes back as a revert carrying no data | |
| 124 | + /// at all rather than as a typed ERC-20 error. Asserting the empty | |
| 125 | + /// data keeps that documented: anything integrating with LOVE has a | |
| 126 | + /// reason to fail, but not a machine-readable one. | |
| 118 | 127 | function test_RevertWhen_DepositWithoutApproval() public { |
| 119 | 128 | vm.prank(alice); |
| 120 | - vm.expectRevert( | |
| 121 | - abi.encodeWithSelector(IERC20Errors.ERC20InsufficientAllowance.selector, address(love), 0, 1 ether) | |
| 122 | - ); | |
| 129 | + vm.expectRevert(bytes("")); | |
| 123 | 130 | love.deposit(1 ether); |
| 124 | 131 | } |
| 125 | 132 | |
| @@ -128,9 +135,7 @@ contract LoveTest is Test { | ||
| 128 | 135 | weth.approve(address(love), 100 ether); |
| 129 | 136 | |
| 130 | 137 | vm.prank(alice); |
| 131 | - vm.expectRevert( | |
| 132 | - abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, alice, 10 ether, 100 ether) | |
| 133 | - ); | |
| 138 | + vm.expectRevert(bytes("")); | |
| 134 | 139 | love.deposit(100 ether); |
| 135 | 140 | } |
| 136 | 141 | |
| @@ -1,22 +1,23 @@ | |||
| 1 | // SPDX-License-Identifier: MIT | 1 | // SPDX-License-Identifier: MIT |
| 2 | pragma solidity ^0.8.30; | 2 | pragma solidity ^0.8.30; |
| 3 | 3 | ||
| 4 | +import {IWETH} from "../src/IWETH.sol"; | ||
| 4 | import {Love} from "../src/Love.sol"; | 5 | import {Love} from "../src/Love.sol"; |
| 5 | -import {MockWETH} from "./mocks/MockWETH.sol"; | 6 | +import {Fixtures} from "./Fixtures.sol"; |
| 6 | import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; | 7 | import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; |
| 7 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | 8 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
| 8 | import {Test} from "forge-std/Test.sol"; | 9 | import {Test} from "forge-std/Test.sol"; |
| 9 | 10 | ||
| 10 | /// @title LoveTest | 11 | /// @title LoveTest |
| 11 | -/// @notice Exercises the peg end to end against a WETH9 stand-in: minting on | 12 | +/// @notice Exercises the peg end to end against real WETH9 bytecode: minting on |
| 12 | /// deposit, redemption on withdraw, the divisibility rule, and the | 13 | /// deposit, redemption on withdraw, the divisibility rule, and the |
| 13 | /// invariant that supply always equals the wETH backing times the rate. | 14 | /// invariant that supply always equals the wETH backing times the rate. |
| 14 | -contract LoveTest is Test { | 15 | +contract LoveTest is Test, Fixtures { |
| 15 | /// @notice The token under test. | 16 | /// @notice The token under test. |
| 16 | Love public love; | 17 | Love public love; |
| 17 | 18 | ||
| 18 | - /// @notice The wETH it is pegged to. | 19 | + /// @notice The wETH it is pegged to, etched from a live chain. |
| 19 | - MockWETH public weth; | 20 | + IWETH public weth; |
| 20 | 21 | ||
| 21 | address alice = makeAddr("alice"); | 22 | address alice = makeAddr("alice"); |
| 22 | address bob = makeAddr("bob"); | 23 | address bob = makeAddr("bob"); |
| @@ -24,9 +25,12 @@ contract LoveTest is Test { | |||
| 24 | 25 | ||
| 25 | uint256 constant RATE = 100_000; | 26 | uint256 constant RATE = 100_000; |
| 26 | 27 | ||
| 28 | + /// @dev The OP Stack predeploy address, so the setup mirrors a real chain. | ||
| 29 | + address constant WETH_AT = 0x4200000000000000000000000000000000000006; | ||
| 30 | + | ||
| 27 | function setUp() public { | 31 | function setUp() public { |
| 28 | - weth = new MockWETH(); | 32 | + (, weth) = setUpChain(WETH_AT, WETH9_OP_LEGACY); |
| 29 | - love = new Love(IERC20(address(weth))); | 33 | + love = new Love(); |
| 30 | 34 | ||
| 31 | _fund(alice, 10 ether); | 35 | _fund(alice, 10 ether); |
| 32 | _fund(bob, 10 ether); | 36 | _fund(bob, 10 ether); |
| @@ -115,11 +119,14 @@ contract LoveTest is Test { | |||
| 115 | assertEq(love.totalSupply(), 0); | 119 | assertEq(love.totalSupply(), 0); |
| 116 | } | 120 | } |
| 117 | 121 | ||
| 122 | + /// @dev WETH9 predates custom errors and guards `transferFrom` with a bare | ||
| 123 | + /// `require`, so a shortfall comes back as a revert carrying no data | ||
| 124 | + /// at all rather than as a typed ERC-20 error. Asserting the empty | ||
| 125 | + /// data keeps that documented: anything integrating with LOVE has a | ||
| 126 | + /// reason to fail, but not a machine-readable one. | ||
| 118 | function test_RevertWhen_DepositWithoutApproval() public { | 127 | function test_RevertWhen_DepositWithoutApproval() public { |
| 119 | vm.prank(alice); | 128 | vm.prank(alice); |
| 120 | - vm.expectRevert( | 129 | + vm.expectRevert(bytes("")); |
| 121 | - abi.encodeWithSelector(IERC20Errors.ERC20InsufficientAllowance.selector, address(love), 0, 1 ether) | ||
| 122 | - ); | ||
| 123 | love.deposit(1 ether); | 130 | love.deposit(1 ether); |
| 124 | } | 131 | } |
| 125 | 132 | ||
| @@ -128,9 +135,7 @@ contract LoveTest is Test { | |||
| 128 | weth.approve(address(love), 100 ether); | 135 | weth.approve(address(love), 100 ether); |
| 129 | 136 | ||
| 130 | vm.prank(alice); | 137 | vm.prank(alice); |
| 131 | - vm.expectRevert( | 138 | + vm.expectRevert(bytes("")); |
| 132 | - abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, alice, 10 ether, 100 ether) | ||
| 133 | - ); | ||
| 134 | love.deposit(100 ether); | 139 | love.deposit(100 ether); |
| 135 | } | 140 | } |
| 136 | 141 | ||
modified
test/LoveCreate2.t.sol +110 -26 | @@ -3,16 +3,20 @@ pragma solidity ^0.8.30; | ||
| 3 | 3 | |
| 4 | 4 | import {LoveScript} from "../script/Love.s.sol"; |
| 5 | 5 | import {Love} from "../src/Love.sol"; |
| 6 | +import {Fixtures} from "./Fixtures.sol"; | |
| 6 | 7 | import {Test} from "forge-std/Test.sol"; |
| 7 | 8 | |
| 8 | 9 | /// @title LoveCreate2Test |
| 9 | -/// @notice The deployment address derives from the salt, the creation code and | |
| 10 | -/// the wETH constructor argument. These tests pin that derivation: same | |
| 11 | -/// salt and same wETH give the same address on any chain, a different | |
| 12 | -/// wETH gives a different one. | |
| 13 | -contract LoveCreate2Test is Test { | |
| 10 | +/// @notice The deployment address now derives from the salt and the creation | |
| 11 | +/// code alone. wETH used to be a constructor argument and so part of | |
| 12 | +/// that creation code, which gave the token a different address on | |
| 13 | +/// every chain whose wETH sat elsewhere; it is read from the registry | |
| 14 | +/// instead. These tests pin the consequence: same salt, same address, | |
| 15 | +/// on any chain, whatever its wETH is and wherever it lives. | |
| 16 | +contract LoveCreate2Test is Test, Fixtures { | |
| 14 | 17 | bytes32 constant SALT = keccak256("LOVE"); |
| 15 | - address constant WETH = 0x4200000000000000000000000000000000000006; | |
| 18 | + address constant OP_STACK_WETH = 0x4200000000000000000000000000000000000006; | |
| 19 | + address constant MAINNET_WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; | |
| 16 | 20 | |
| 17 | 21 | LoveScript script; |
| 18 | 22 | |
| @@ -23,19 +27,56 @@ contract LoveCreate2Test is Test { | ||
| 23 | 27 | assertGt(CREATE2_FACTORY.code.length, 0, "create2 deployer missing"); |
| 24 | 28 | } |
| 25 | 29 | |
| 30 | + /*////////////////////////////////////////////////////////////// | |
| 31 | + THE REGISTRY | |
| 32 | + //////////////////////////////////////////////////////////////*/ | |
| 33 | + | |
| 34 | + /// @dev `Love` has the registry's address compiled in, so if the registry's | |
| 35 | + /// creation code moves — an edit to the contract, a solc bump, a | |
| 36 | + /// change of optimizer settings — this fails rather than shipping a | |
| 37 | + /// token pointing at an empty address. | |
| 38 | + function test_LoveRegistryConstantMatchesDeterministicAddress() public { | |
| 39 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); | |
| 40 | + | |
| 41 | + assertEq(new Love().REGISTRY(), script.registryAddress()); | |
| 42 | + } | |
| 43 | + | |
| 44 | + function test_RegistryLandsAtTheAddressLoveExpects() public { | |
| 45 | + assertEq(address(deployRegistry()), script.registryAddress()); | |
| 46 | + } | |
| 47 | + | |
| 48 | + /// @dev The registry takes no constructor arguments, which is what makes | |
| 49 | + /// its address the same everywhere. | |
| 50 | + function test_RegistryAddressIsChainAgnostic() public { | |
| 51 | + address onThisChain = script.registryAddress(); | |
| 52 | + | |
| 53 | + vm.chainId(137); | |
| 54 | + assertEq(script.registryAddress(), onThisChain); | |
| 55 | + | |
| 56 | + vm.chainId(42_161); | |
| 57 | + assertEq(script.registryAddress(), onThisChain); | |
| 58 | + } | |
| 59 | + | |
| 60 | + /*////////////////////////////////////////////////////////////// | |
| 61 | + THE DERIVATION | |
| 62 | + //////////////////////////////////////////////////////////////*/ | |
| 63 | + | |
| 26 | 64 | function test_DeploysAtPredictedAddress() public { |
| 27 | - address predicted = vm.computeCreate2Address(SALT, keccak256(script.initCode(WETH)), CREATE2_FACTORY); | |
| 65 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); | |
| 28 | 66 | |
| 29 | - Love deployed = Love(_deploy(SALT, WETH)); | |
| 67 | + address predicted = vm.computeCreate2Address(SALT, keccak256(script.initCode()), CREATE2_FACTORY); | |
| 68 | + Love deployed = Love(_deploy(SALT)); | |
| 30 | 69 | |
| 31 | 70 | assertEq(address(deployed), predicted); |
| 32 | 71 | assertEq(deployed.symbol(), "LOVE"); |
| 33 | - assertEq(address(deployed.WETH()), WETH); | |
| 72 | + assertEq(address(deployed.WETH()), OP_STACK_WETH); | |
| 34 | 73 | } |
| 35 | 74 | |
| 36 | 75 | /// @dev What the deploy script prints must be what the chain gives back. |
| 37 | 76 | function test_ScriptPredictionMatchesDeployment() public { |
| 38 | - assertEq(script.predict(SALT, WETH), _deploy(SALT, WETH)); | |
| 77 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); | |
| 78 | + | |
| 79 | + assertEq(script.predict(SALT), _deploy(SALT)); | |
| 39 | 80 | } |
| 40 | 81 | |
| 41 | 82 | function test_ScriptUsesKeccakOfLoveAsDefaultSalt() public view { |
| @@ -43,46 +84,89 @@ contract LoveCreate2Test is Test { | ||
| 43 | 84 | } |
| 44 | 85 | |
| 45 | 86 | function test_ScriptDefaultsToOpStackWeth() public view { |
| 46 | - assertEq(script.DEFAULT_WETH(), WETH); | |
| 87 | + assertEq(script.DEFAULT_WETH(), OP_STACK_WETH); | |
| 47 | 88 | } |
| 48 | 89 | |
| 49 | - /// @dev Same salt, same code, same wETH, same address. | |
| 90 | + /// @dev Same salt, same code, same address — and unlike before, no wETH | |
| 91 | + /// argument that could make the answer differ. | |
| 50 | 92 | function test_PredictionIsChainAgnostic() public { |
| 51 | - address onThisChain = script.predict(SALT, WETH); | |
| 93 | + address onThisChain = script.predict(SALT); | |
| 52 | 94 | |
| 53 | 95 | vm.chainId(137); |
| 54 | - assertEq(script.predict(SALT, WETH), onThisChain); | |
| 96 | + assertEq(script.predict(SALT), onThisChain); | |
| 55 | 97 | |
| 56 | 98 | vm.chainId(42_161); |
| 57 | - assertEq(script.predict(SALT, WETH), onThisChain); | |
| 99 | + assertEq(script.predict(SALT), onThisChain); | |
| 58 | 100 | } |
| 59 | 101 | |
| 60 | 102 | function test_DifferentSaltsGiveDifferentAddresses() public view { |
| 61 | - assertTrue(script.predict(SALT, WETH) != script.predict(keccak256("LOVE2"), WETH)); | |
| 103 | + assertTrue(script.predict(SALT) != script.predict(keccak256("LOVE2"))); | |
| 62 | 104 | } |
| 63 | 105 | |
| 64 | - /// @dev The wETH address is part of the creation code, so chains with their | |
| 65 | - /// own wETH get their own token address. | |
| 66 | - function test_DifferentWethGivesDifferentAddress() public view { | |
| 67 | - assertTrue(script.predict(SALT, WETH) != script.predict(SALT, address(0xBEEF))); | |
| 106 | + /*////////////////////////////////////////////////////////////// | |
| 107 | + THE POINT OF ALL THIS | |
| 108 | + //////////////////////////////////////////////////////////////*/ | |
| 109 | + | |
| 110 | + /// @dev The test that replaces `test_DifferentWethGivesDifferentAddress`. | |
| 111 | + /// Two chains, two different wETH implementations at two different | |
| 112 | + /// addresses, one LOVE address. This is what the registry buys. | |
| 113 | + function test_SameAddressOnChainsWithDifferentWeth() public { | |
| 114 | + // A snapshot stands in for a second chain. Etching the code away is | |
| 115 | + // not enough: a deployed account keeps its nonce, and CREATE2 refuses | |
| 116 | + // to build over that, so the registry could never be redeployed. | |
| 117 | + uint256 freshChain = vm.snapshotState(); | |
| 118 | + | |
| 119 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); | |
| 120 | + address onOpStack = _deploy(SALT); | |
| 121 | + assertEq(address(Love(onOpStack).WETH()), OP_STACK_WETH); | |
| 122 | + | |
| 123 | + vm.revertToState(freshChain); | |
| 124 | + | |
| 125 | + // Different chain id, a different wETH implementation, at a different | |
| 126 | + // address. | |
| 127 | + vm.chainId(1); | |
| 128 | + setUpChain(MAINNET_WETH, WETH9_CANONICAL); | |
| 129 | + address onMainnet = _deploy(SALT); | |
| 130 | + assertEq(address(Love(onMainnet).WETH()), MAINNET_WETH); | |
| 131 | + | |
| 132 | + assertEq(onOpStack, onMainnet, "LOVE must land at one address on both chains"); | |
| 68 | 133 | } |
| 69 | 134 | |
| 70 | 135 | /// @dev Redeploying with the same salt must fail, not silently return the |
| 71 | 136 | /// existing token. |
| 72 | 137 | function test_RevertWhen_RedeployingWithSameSalt() public { |
| 73 | - _deploy(SALT, WETH); | |
| 138 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); | |
| 139 | + _deploy(SALT); | |
| 74 | 140 | |
| 75 | - (bool ok,) = CREATE2_FACTORY.call(abi.encodePacked(SALT, script.initCode(WETH))); | |
| 141 | + (bool ok,) = CREATE2_FACTORY.call(abi.encodePacked(SALT, script.initCode())); | |
| 76 | 142 | assertFalse(ok); |
| 77 | 143 | } |
| 78 | 144 | |
| 79 | - function testFuzz_PredictionMatchesDeployment(bytes32 salt, address weth) public { | |
| 80 | - assertEq(script.predict(salt, weth), _deploy(salt, weth)); | |
| 145 | + function testFuzz_PredictionMatchesDeployment(bytes32 salt) public { | |
| 146 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); | |
| 147 | + | |
| 148 | + assertEq(script.predict(salt), _deploy(salt)); | |
| 149 | + } | |
| 150 | + | |
| 151 | + /*////////////////////////////////////////////////////////////// | |
| 152 | + CONSTRUCTOR GUARDS | |
| 153 | + //////////////////////////////////////////////////////////////*/ | |
| 154 | + | |
| 155 | + function test_RevertWhen_RegistryNotDeployed() public { | |
| 156 | + vm.expectRevert(abi.encodeWithSelector(Love.RegistryNotDeployed.selector, script.registryAddress())); | |
| 157 | + new Love(); | |
| 158 | + } | |
| 159 | + | |
| 160 | + function test_RevertWhen_WethNotRegistered() public { | |
| 161 | + deployRegistry(); | |
| 162 | + | |
| 163 | + vm.expectRevert(abi.encodeWithSelector(Love.WethNotRegistered.selector, script.registryAddress())); | |
| 164 | + new Love(); | |
| 81 | 165 | } |
| 82 | 166 | |
| 83 | 167 | /// @dev Deploys through the canonical deterministic deployer, as the script does. |
| 84 | - function _deploy(bytes32 salt, address weth) internal returns (address deployed) { | |
| 85 | - (bool ok, bytes memory ret) = CREATE2_FACTORY.call(abi.encodePacked(salt, script.initCode(weth))); | |
| 168 | + function _deploy(bytes32 salt) internal returns (address deployed) { | |
| 169 | + (bool ok, bytes memory ret) = CREATE2_FACTORY.call(abi.encodePacked(salt, script.initCode())); | |
| 86 | 170 | require(ok, "create2 deployment failed"); |
| 87 | 171 | // casting to 'bytes20' is safe because the deployer returns the 20-byte address, raw |
| 88 | 172 | // forge-lint: disable-next-line(unsafe-typecast) |
| @@ -3,16 +3,20 @@ pragma solidity ^0.8.30; | |||
| 3 | 3 | ||
| 4 | import {LoveScript} from "../script/Love.s.sol"; | 4 | import {LoveScript} from "../script/Love.s.sol"; |
| 5 | import {Love} from "../src/Love.sol"; | 5 | import {Love} from "../src/Love.sol"; |
| 6 | +import {Fixtures} from "./Fixtures.sol"; | ||
| 6 | import {Test} from "forge-std/Test.sol"; | 7 | import {Test} from "forge-std/Test.sol"; |
| 7 | 8 | ||
| 8 | /// @title LoveCreate2Test | 9 | /// @title LoveCreate2Test |
| 9 | -/// @notice The deployment address derives from the salt, the creation code and | 10 | +/// @notice The deployment address now derives from the salt and the creation |
| 10 | -/// the wETH constructor argument. These tests pin that derivation: same | 11 | +/// code alone. wETH used to be a constructor argument and so part of |
| 11 | -/// salt and same wETH give the same address on any chain, a different | 12 | +/// that creation code, which gave the token a different address on |
| 12 | -/// wETH gives a different one. | 13 | +/// every chain whose wETH sat elsewhere; it is read from the registry |
| 13 | -contract LoveCreate2Test is Test { | 14 | +/// instead. These tests pin the consequence: same salt, same address, |
| 15 | +/// on any chain, whatever its wETH is and wherever it lives. | ||
| 16 | +contract LoveCreate2Test is Test, Fixtures { | ||
| 14 | bytes32 constant SALT = keccak256("LOVE"); | 17 | bytes32 constant SALT = keccak256("LOVE"); |
| 15 | - address constant WETH = 0x4200000000000000000000000000000000000006; | 18 | + address constant OP_STACK_WETH = 0x4200000000000000000000000000000000000006; |
| 19 | + address constant MAINNET_WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; | ||
| 16 | 20 | ||
| 17 | LoveScript script; | 21 | LoveScript script; |
| 18 | 22 | ||
| @@ -23,19 +27,56 @@ contract LoveCreate2Test is Test { | |||
| 23 | assertGt(CREATE2_FACTORY.code.length, 0, "create2 deployer missing"); | 27 | assertGt(CREATE2_FACTORY.code.length, 0, "create2 deployer missing"); |
| 24 | } | 28 | } |
| 25 | 29 | ||
| 30 | + /*////////////////////////////////////////////////////////////// | ||
| 31 | + THE REGISTRY | ||
| 32 | + //////////////////////////////////////////////////////////////*/ | ||
| 33 | + | ||
| 34 | + /// @dev `Love` has the registry's address compiled in, so if the registry's | ||
| 35 | + /// creation code moves — an edit to the contract, a solc bump, a | ||
| 36 | + /// change of optimizer settings — this fails rather than shipping a | ||
| 37 | + /// token pointing at an empty address. | ||
| 38 | + function test_LoveRegistryConstantMatchesDeterministicAddress() public { | ||
| 39 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); | ||
| 40 | + | ||
| 41 | + assertEq(new Love().REGISTRY(), script.registryAddress()); | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + function test_RegistryLandsAtTheAddressLoveExpects() public { | ||
| 45 | + assertEq(address(deployRegistry()), script.registryAddress()); | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + /// @dev The registry takes no constructor arguments, which is what makes | ||
| 49 | + /// its address the same everywhere. | ||
| 50 | + function test_RegistryAddressIsChainAgnostic() public { | ||
| 51 | + address onThisChain = script.registryAddress(); | ||
| 52 | + | ||
| 53 | + vm.chainId(137); | ||
| 54 | + assertEq(script.registryAddress(), onThisChain); | ||
| 55 | + | ||
| 56 | + vm.chainId(42_161); | ||
| 57 | + assertEq(script.registryAddress(), onThisChain); | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + /*////////////////////////////////////////////////////////////// | ||
| 61 | + THE DERIVATION | ||
| 62 | + //////////////////////////////////////////////////////////////*/ | ||
| 63 | + | ||
| 26 | function test_DeploysAtPredictedAddress() public { | 64 | function test_DeploysAtPredictedAddress() public { |
| 27 | - address predicted = vm.computeCreate2Address(SALT, keccak256(script.initCode(WETH)), CREATE2_FACTORY); | 65 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); |
| 28 | 66 | ||
| 29 | - Love deployed = Love(_deploy(SALT, WETH)); | 67 | + address predicted = vm.computeCreate2Address(SALT, keccak256(script.initCode()), CREATE2_FACTORY); |
| 68 | + Love deployed = Love(_deploy(SALT)); | ||
| 30 | 69 | ||
| 31 | assertEq(address(deployed), predicted); | 70 | assertEq(address(deployed), predicted); |
| 32 | assertEq(deployed.symbol(), "LOVE"); | 71 | assertEq(deployed.symbol(), "LOVE"); |
| 33 | - assertEq(address(deployed.WETH()), WETH); | 72 | + assertEq(address(deployed.WETH()), OP_STACK_WETH); |
| 34 | } | 73 | } |
| 35 | 74 | ||
| 36 | /// @dev What the deploy script prints must be what the chain gives back. | 75 | /// @dev What the deploy script prints must be what the chain gives back. |
| 37 | function test_ScriptPredictionMatchesDeployment() public { | 76 | function test_ScriptPredictionMatchesDeployment() public { |
| 38 | - assertEq(script.predict(SALT, WETH), _deploy(SALT, WETH)); | 77 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); |
| 78 | + | ||
| 79 | + assertEq(script.predict(SALT), _deploy(SALT)); | ||
| 39 | } | 80 | } |
| 40 | 81 | ||
| 41 | function test_ScriptUsesKeccakOfLoveAsDefaultSalt() public view { | 82 | function test_ScriptUsesKeccakOfLoveAsDefaultSalt() public view { |
| @@ -43,46 +84,89 @@ contract LoveCreate2Test is Test { | |||
| 43 | } | 84 | } |
| 44 | 85 | ||
| 45 | function test_ScriptDefaultsToOpStackWeth() public view { | 86 | function test_ScriptDefaultsToOpStackWeth() public view { |
| 46 | - assertEq(script.DEFAULT_WETH(), WETH); | 87 | + assertEq(script.DEFAULT_WETH(), OP_STACK_WETH); |
| 47 | } | 88 | } |
| 48 | 89 | ||
| 49 | - /// @dev Same salt, same code, same wETH, same address. | 90 | + /// @dev Same salt, same code, same address — and unlike before, no wETH |
| 91 | + /// argument that could make the answer differ. | ||
| 50 | function test_PredictionIsChainAgnostic() public { | 92 | function test_PredictionIsChainAgnostic() public { |
| 51 | - address onThisChain = script.predict(SALT, WETH); | 93 | + address onThisChain = script.predict(SALT); |
| 52 | 94 | ||
| 53 | vm.chainId(137); | 95 | vm.chainId(137); |
| 54 | - assertEq(script.predict(SALT, WETH), onThisChain); | 96 | + assertEq(script.predict(SALT), onThisChain); |
| 55 | 97 | ||
| 56 | vm.chainId(42_161); | 98 | vm.chainId(42_161); |
| 57 | - assertEq(script.predict(SALT, WETH), onThisChain); | 99 | + assertEq(script.predict(SALT), onThisChain); |
| 58 | } | 100 | } |
| 59 | 101 | ||
| 60 | function test_DifferentSaltsGiveDifferentAddresses() public view { | 102 | function test_DifferentSaltsGiveDifferentAddresses() public view { |
| 61 | - assertTrue(script.predict(SALT, WETH) != script.predict(keccak256("LOVE2"), WETH)); | 103 | + assertTrue(script.predict(SALT) != script.predict(keccak256("LOVE2"))); |
| 62 | } | 104 | } |
| 63 | 105 | ||
| 64 | - /// @dev The wETH address is part of the creation code, so chains with their | 106 | + /*////////////////////////////////////////////////////////////// |
| 65 | - /// own wETH get their own token address. | 107 | + THE POINT OF ALL THIS |
| 66 | - function test_DifferentWethGivesDifferentAddress() public view { | 108 | + //////////////////////////////////////////////////////////////*/ |
| 67 | - assertTrue(script.predict(SALT, WETH) != script.predict(SALT, address(0xBEEF))); | 109 | + |
| 110 | + /// @dev The test that replaces `test_DifferentWethGivesDifferentAddress`. | ||
| 111 | + /// Two chains, two different wETH implementations at two different | ||
| 112 | + /// addresses, one LOVE address. This is what the registry buys. | ||
| 113 | + function test_SameAddressOnChainsWithDifferentWeth() public { | ||
| 114 | + // A snapshot stands in for a second chain. Etching the code away is | ||
| 115 | + // not enough: a deployed account keeps its nonce, and CREATE2 refuses | ||
| 116 | + // to build over that, so the registry could never be redeployed. | ||
| 117 | + uint256 freshChain = vm.snapshotState(); | ||
| 118 | + | ||
| 119 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); | ||
| 120 | + address onOpStack = _deploy(SALT); | ||
| 121 | + assertEq(address(Love(onOpStack).WETH()), OP_STACK_WETH); | ||
| 122 | + | ||
| 123 | + vm.revertToState(freshChain); | ||
| 124 | + | ||
| 125 | + // Different chain id, a different wETH implementation, at a different | ||
| 126 | + // address. | ||
| 127 | + vm.chainId(1); | ||
| 128 | + setUpChain(MAINNET_WETH, WETH9_CANONICAL); | ||
| 129 | + address onMainnet = _deploy(SALT); | ||
| 130 | + assertEq(address(Love(onMainnet).WETH()), MAINNET_WETH); | ||
| 131 | + | ||
| 132 | + assertEq(onOpStack, onMainnet, "LOVE must land at one address on both chains"); | ||
| 68 | } | 133 | } |
| 69 | 134 | ||
| 70 | /// @dev Redeploying with the same salt must fail, not silently return the | 135 | /// @dev Redeploying with the same salt must fail, not silently return the |
| 71 | /// existing token. | 136 | /// existing token. |
| 72 | function test_RevertWhen_RedeployingWithSameSalt() public { | 137 | function test_RevertWhen_RedeployingWithSameSalt() public { |
| 73 | - _deploy(SALT, WETH); | 138 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); |
| 139 | + _deploy(SALT); | ||
| 74 | 140 | ||
| 75 | - (bool ok,) = CREATE2_FACTORY.call(abi.encodePacked(SALT, script.initCode(WETH))); | 141 | + (bool ok,) = CREATE2_FACTORY.call(abi.encodePacked(SALT, script.initCode())); |
| 76 | assertFalse(ok); | 142 | assertFalse(ok); |
| 77 | } | 143 | } |
| 78 | 144 | ||
| 79 | - function testFuzz_PredictionMatchesDeployment(bytes32 salt, address weth) public { | 145 | + function testFuzz_PredictionMatchesDeployment(bytes32 salt) public { |
| 80 | - assertEq(script.predict(salt, weth), _deploy(salt, weth)); | 146 | + setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); |
| 147 | + | ||
| 148 | + assertEq(script.predict(salt), _deploy(salt)); | ||
| 149 | + } | ||
| 150 | + | ||
| 151 | + /*////////////////////////////////////////////////////////////// | ||
| 152 | + CONSTRUCTOR GUARDS | ||
| 153 | + //////////////////////////////////////////////////////////////*/ | ||
| 154 | + | ||
| 155 | + function test_RevertWhen_RegistryNotDeployed() public { | ||
| 156 | + vm.expectRevert(abi.encodeWithSelector(Love.RegistryNotDeployed.selector, script.registryAddress())); | ||
| 157 | + new Love(); | ||
| 158 | + } | ||
| 159 | + | ||
| 160 | + function test_RevertWhen_WethNotRegistered() public { | ||
| 161 | + deployRegistry(); | ||
| 162 | + | ||
| 163 | + vm.expectRevert(abi.encodeWithSelector(Love.WethNotRegistered.selector, script.registryAddress())); | ||
| 164 | + new Love(); | ||
| 81 | } | 165 | } |
| 82 | 166 | ||
| 83 | /// @dev Deploys through the canonical deterministic deployer, as the script does. | 167 | /// @dev Deploys through the canonical deterministic deployer, as the script does. |
| 84 | - function _deploy(bytes32 salt, address weth) internal returns (address deployed) { | 168 | + function _deploy(bytes32 salt) internal returns (address deployed) { |
| 85 | - (bool ok, bytes memory ret) = CREATE2_FACTORY.call(abi.encodePacked(salt, script.initCode(weth))); | 169 | + (bool ok, bytes memory ret) = CREATE2_FACTORY.call(abi.encodePacked(salt, script.initCode())); |
| 86 | require(ok, "create2 deployment failed"); | 170 | require(ok, "create2 deployment failed"); |
| 87 | // casting to 'bytes20' is safe because the deployer returns the 20-byte address, raw | 171 | // casting to 'bytes20' is safe because the deployer returns the 20-byte address, raw |
| 88 | // forge-lint: disable-next-line(unsafe-typecast) | 172 | // forge-lint: disable-next-line(unsafe-typecast) |
added
test/WETHRegistry.t.sol +281 -0 | new file mode 100644 | ||
| @@ -0,0 +1,281 @@ | ||
| 1 | +// SPDX-License-Identifier: MIT | |
| 2 | +pragma solidity ^0.8.30; | |
| 3 | + | |
| 4 | +import {IWETH} from "../src/IWETH.sol"; | |
| 5 | +import {WETHRegistry} from "../src/WETHRegistry.sol"; | |
| 6 | +import {Fixtures} from "./Fixtures.sol"; | |
| 7 | +import {MockWETH} from "./mocks/MockWETH.sol"; | |
| 8 | +import {Test} from "forge-std/Test.sol"; | |
| 9 | + | |
| 10 | +/// @notice A registrar with no payable fallback, which is what a deployer | |
| 11 | +/// contract or a multisig looks like from the registry's side. | |
| 12 | +contract PlainRegistrar { | |
| 13 | + function register(WETHRegistry registry, IWETH weth) external payable { | |
| 14 | + registry.register{value: msg.value}(weth); | |
| 15 | + } | |
| 16 | +} | |
| 17 | + | |
| 18 | +/// @title WETHRegistryTest | |
| 19 | +/// @notice Covers the two gates the registry exists to run — is this code a | |
| 20 | +/// reviewed wETH, and does it actually wrap — plus the write-once rule | |
| 21 | +/// that stops the token under LOVE's peg from ever being swapped. | |
| 22 | +contract WETHRegistryTest is Test, Fixtures { | |
| 23 | + WETHRegistry registry; | |
| 24 | + | |
| 25 | + /// @dev Read once in `setUp`. Calling `registry.PROBE()` inline after | |
| 26 | + /// `vm.expectRevert` would bind the expectation to that call instead | |
| 27 | + /// of to `register`. | |
| 28 | + uint256 probe; | |
| 29 | + | |
| 30 | + address constant WETH_AT = 0x4200000000000000000000000000000000000006; | |
| 31 | + address constant OTHER_WETH_AT = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; | |
| 32 | + | |
| 33 | + event Registered(IWETH indexed weth, address indexed registrar, bytes32 codeHash); | |
| 34 | + | |
| 35 | + function setUp() public { | |
| 36 | + registry = deployRegistry(); | |
| 37 | + probe = registry.PROBE(); | |
| 38 | + } | |
| 39 | + | |
| 40 | + /*////////////////////////////////////////////////////////////// | |
| 41 | + THE ALLOWLIST | |
| 42 | + //////////////////////////////////////////////////////////////*/ | |
| 43 | + | |
| 44 | + /// @dev The ten entries, reproducible from chain state with | |
| 45 | + /// `script/weth-codehashes.sh`. Listing them here means a hash edited | |
| 46 | + /// in the contract shows up as a failing test rather than as a silent | |
| 47 | + /// change to what LOVE will accept. | |
| 48 | + function test_KnownImplementations() public view { | |
| 49 | + bytes32[10] memory allowed = [ | |
| 50 | + // OP Stack legacy WETH9 | |
| 51 | + bytes32(0x779bbf2a738ef09d961c945116197e2ac764c1b39304b2b4418cd4e42668b173), | |
| 52 | + 0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c, | |
| 53 | + 0x557c8e14d33f7cd67cad0141e1a49ebf3488a447fc3df7aa66b127778a0383d1, | |
| 54 | + 0xf35fe602ba2a3b96f2e27ff7c8b8010800a8d0d616a5fb1f902e087b590355f3, | |
| 55 | + // Canonical WETH9 | |
| 56 | + 0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23, | |
| 57 | + 0xa670ec6c272ddec6d328d6f3d5cad65a841a6ab45e8e5cf825150eb458be4f1f, | |
| 58 | + 0x032e9cab14331328530468e54f1b91777b4d5c9dbbb400884badb32bc4113585, | |
| 59 | + // OP Stack WETH, solc 0.8.15 | |
| 60 | + 0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012, | |
| 61 | + // Taiko | |
| 62 | + 0x9f3d95086909fce850d997158aba31abe26c3aad6a413107ca0bf9d53a7c42e9, | |
| 63 | + // Scroll | |
| 64 | + 0xe8c4073351c26b9831c1e5af153b9be4713a4af9edfdf32b58077b735e120f14 | |
| 65 | + ]; | |
| 66 | + | |
| 67 | + for (uint256 i = 0; i < allowed.length; i++) { | |
| 68 | + assertTrue(registry.isKnownImplementation(allowed[i]), "allowlisted hash rejected"); | |
| 69 | + } | |
| 70 | + } | |
| 71 | + | |
| 72 | + function test_EmptyCodeHashIsNotKnown() public view { | |
| 73 | + assertFalse(registry.isKnownImplementation(bytes32(0))); | |
| 74 | + assertFalse(registry.isKnownImplementation(keccak256(""))); | |
| 75 | + } | |
| 76 | + | |
| 77 | + function testFuzz_UnknownHashesAreRejected(bytes32 codeHash) public view { | |
| 78 | + vm.assume(!registry.isKnownImplementation(codeHash)); | |
| 79 | + assertFalse(registry.isKnownImplementation(codeHash)); | |
| 80 | + } | |
| 81 | + | |
| 82 | + /*////////////////////////////////////////////////////////////// | |
| 83 | + REGISTERING | |
| 84 | + //////////////////////////////////////////////////////////////*/ | |
| 85 | + | |
| 86 | + function test_RegistersOpStackWeth() public { | |
| 87 | + IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY); | |
| 88 | + register(registry, weth); | |
| 89 | + | |
| 90 | + assertEq(address(registry.weth()), WETH_AT); | |
| 91 | + } | |
| 92 | + | |
| 93 | + function test_RegistersCanonicalWeth() public { | |
| 94 | + IWETH weth = etchWeth(OTHER_WETH_AT, WETH9_CANONICAL); | |
| 95 | + register(registry, weth); | |
| 96 | + | |
| 97 | + assertEq(address(registry.weth()), OTHER_WETH_AT); | |
| 98 | + } | |
| 99 | + | |
| 100 | + function test_EmitsRegistered() public { | |
| 101 | + IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY); | |
| 102 | + deal(address(this), 1 ether); | |
| 103 | + | |
| 104 | + vm.expectEmit(true, true, true, true); | |
| 105 | + emit Registered(weth, address(this), WETH_AT.codehash); | |
| 106 | + registry.register{value: probe}(weth); | |
| 107 | + } | |
| 108 | + | |
| 109 | + function test_NothingRegisteredInitially() public view { | |
| 110 | + assertEq(address(registry.weth()), address(0)); | |
| 111 | + } | |
| 112 | + | |
| 113 | + /// @dev The reason the probe wei is not refunded: paying it back means | |
| 114 | + /// calling the registrar with value, which a contract without a | |
| 115 | + /// payable fallback cannot accept. | |
| 116 | + function test_RegistrarWithoutPayableFallbackCanRegister() public { | |
| 117 | + IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY); | |
| 118 | + PlainRegistrar registrar = new PlainRegistrar(); | |
| 119 | + deal(address(this), 1 ether); | |
| 120 | + | |
| 121 | + registrar.register{value: probe}(registry, weth); | |
| 122 | + | |
| 123 | + assertEq(address(registry.weth()), WETH_AT); | |
| 124 | + } | |
| 125 | + | |
| 126 | + function test_ProbeWeiStaysInTheRegistry() public { | |
| 127 | + IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY); | |
| 128 | + register(registry, weth); | |
| 129 | + | |
| 130 | + assertEq(address(registry).balance, probe); | |
| 131 | + } | |
| 132 | + | |
| 133 | + /*////////////////////////////////////////////////////////////// | |
| 134 | + THE BYTECODE GATE | |
| 135 | + //////////////////////////////////////////////////////////////*/ | |
| 136 | + | |
| 137 | + /// @dev `MockWETH` wraps ether correctly and would sail through the | |
| 138 | + /// behavioural probe. It is rejected on its code alone, which is the | |
| 139 | + /// gate doing exactly what it is for: behaving well today says | |
| 140 | + /// nothing about what else the code can do. | |
| 141 | + function test_RevertWhen_CodeIsNotAReviewedImplementation() public { | |
| 142 | + MockWETH mock = new MockWETH(); | |
| 143 | + deal(address(this), 1 ether); | |
| 144 | + | |
| 145 | + vm.expectRevert( | |
| 146 | + abi.encodeWithSelector( | |
| 147 | + WETHRegistry.UnknownImplementation.selector, IWETH(address(mock)), address(mock).codehash | |
| 148 | + ) | |
| 149 | + ); | |
| 150 | + registry.register{value: probe}(IWETH(address(mock))); | |
| 151 | + } | |
| 152 | + | |
| 153 | + function test_RevertWhen_CandidateHasNoCode() public { | |
| 154 | + address empty = makeAddr("empty"); | |
| 155 | + deal(address(this), 1 ether); | |
| 156 | + | |
| 157 | + vm.expectRevert(abi.encodeWithSelector(WETHRegistry.UnknownImplementation.selector, IWETH(empty), bytes32(0))); | |
| 158 | + registry.register{value: probe}(IWETH(empty)); | |
| 159 | + } | |
| 160 | + | |
| 161 | + /*////////////////////////////////////////////////////////////// | |
| 162 | + THE BEHAVIOURAL PROBE | |
| 163 | + //////////////////////////////////////////////////////////////*/ | |
| 164 | + | |
| 165 | + /// @dev Mocking `balanceOf` leaves the codehash untouched, so the | |
| 166 | + /// candidate still passes the allowlist and the probe is what has to | |
| 167 | + /// catch it. A wrapper that mints nothing on deposit fails here. | |
| 168 | + function test_RevertWhen_DepositMintsNothing() public { | |
| 169 | + IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY); | |
| 170 | + deal(address(this), 1 ether); | |
| 171 | + | |
| 172 | + vm.mockCall(WETH_AT, abi.encodeCall(weth.balanceOf, (address(registry))), abi.encode(uint256(0))); | |
| 173 | + | |
| 174 | + vm.expectRevert(abi.encodeWithSelector(WETHRegistry.DepositMismatch.selector, 1, 0)); | |
| 175 | + registry.register{value: probe}(weth); | |
| 176 | + } | |
| 177 | + | |
| 178 | + /// @dev Mints on deposit, but burns nothing on withdraw: the balance is | |
| 179 | + /// read three times, and the third answer is the one that lies. | |
| 180 | + function test_RevertWhen_WithdrawBurnsNothing() public { | |
| 181 | + IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY); | |
| 182 | + deal(address(this), 1 ether); | |
| 183 | + | |
| 184 | + bytes[] memory balances = new bytes[](3); | |
| 185 | + balances[0] = abi.encode(uint256(0)); // before | |
| 186 | + balances[1] = abi.encode(uint256(1)); // after deposit, correct | |
| 187 | + balances[2] = abi.encode(uint256(1)); // after withdraw, should be 0 | |
| 188 | + vm.mockCalls(WETH_AT, abi.encodeCall(weth.balanceOf, (address(registry))), balances); | |
| 189 | + | |
| 190 | + vm.expectRevert(abi.encodeWithSelector(WETHRegistry.WithdrawMismatch.selector, 0, 1)); | |
| 191 | + registry.register{value: probe}(weth); | |
| 192 | + } | |
| 193 | + | |
| 194 | + /// @dev Books the withdrawal correctly but keeps the ether. The balance | |
| 195 | + /// check after the round trip is the only thing that notices. | |
| 196 | + function test_RevertWhen_WithdrawKeepsTheEther() public { | |
| 197 | + IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY); | |
| 198 | + deal(address(this), 1 ether); | |
| 199 | + | |
| 200 | + vm.mockCall(WETH_AT, abi.encodeCall(weth.withdraw, (1)), ""); | |
| 201 | + | |
| 202 | + bytes[] memory balances = new bytes[](3); | |
| 203 | + balances[0] = abi.encode(uint256(0)); | |
| 204 | + balances[1] = abi.encode(uint256(1)); | |
| 205 | + balances[2] = abi.encode(uint256(0)); | |
| 206 | + vm.mockCalls(WETH_AT, abi.encodeCall(weth.balanceOf, (address(registry))), balances); | |
| 207 | + | |
| 208 | + vm.expectRevert(abi.encodeWithSelector(WETHRegistry.EtherNotReturned.selector, 1, 0)); | |
| 209 | + registry.register{value: probe}(weth); | |
| 210 | + } | |
| 211 | + | |
| 212 | + /*////////////////////////////////////////////////////////////// | |
| 213 | + THE PROBE FEE | |
| 214 | + //////////////////////////////////////////////////////////////*/ | |
| 215 | + | |
| 216 | + function test_RevertWhen_NoProbeValue() public { | |
| 217 | + IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY); | |
| 218 | + | |
| 219 | + vm.expectRevert(abi.encodeWithSelector(WETHRegistry.ProbeValueRequired.selector, 0)); | |
| 220 | + registry.register(weth); | |
| 221 | + } | |
| 222 | + | |
| 223 | + function testFuzz_RevertWhen_WrongProbeValue(uint96 value) public { | |
| 224 | + vm.assume(value != probe); | |
| 225 | + IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY); | |
| 226 | + deal(address(this), uint256(value) + 1 ether); | |
| 227 | + | |
| 228 | + vm.expectRevert(abi.encodeWithSelector(WETHRegistry.ProbeValueRequired.selector, value)); | |
| 229 | + registry.register{value: value}(weth); | |
| 230 | + } | |
| 231 | + | |
| 232 | + /*////////////////////////////////////////////////////////////// | |
| 233 | + WRITE-ONCE | |
| 234 | + //////////////////////////////////////////////////////////////*/ | |
| 235 | + | |
| 236 | + function test_RevertWhen_AlreadyRegistered() public { | |
| 237 | + IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY); | |
| 238 | + register(registry, weth); | |
| 239 | + | |
| 240 | + deal(address(this), 1 ether); | |
| 241 | + vm.expectRevert(abi.encodeWithSelector(WETHRegistry.AlreadyRegistered.selector, weth)); | |
| 242 | + registry.register{value: probe}(weth); | |
| 243 | + } | |
| 244 | + | |
| 245 | + /// @dev Including with a second, equally valid wETH — the first answer is | |
| 246 | + /// final, so nothing can move the token under a live peg. | |
| 247 | + function test_RevertWhen_ReplacingWithAnotherValidWeth() public { | |
| 248 | + IWETH first = etchWeth(WETH_AT, WETH9_OP_LEGACY); | |
| 249 | + register(registry, first); | |
| 250 | + | |
| 251 | + IWETH second = etchWeth(OTHER_WETH_AT, WETH9_CANONICAL); | |
| 252 | + deal(address(this), 1 ether); | |
| 253 | + | |
| 254 | + vm.expectRevert(abi.encodeWithSelector(WETHRegistry.AlreadyRegistered.selector, first)); | |
| 255 | + registry.register{value: probe}(second); | |
| 256 | + | |
| 257 | + assertEq(address(registry.weth()), WETH_AT); | |
| 258 | + } | |
| 259 | + | |
| 260 | + /*////////////////////////////////////////////////////////////// | |
| 261 | + NOT A WALLET | |
| 262 | + //////////////////////////////////////////////////////////////*/ | |
| 263 | + | |
| 264 | + /// @dev The registry has no owner and no way to move ether out, so it must | |
| 265 | + /// not accept any outside a probe. | |
| 266 | + function test_RevertWhen_EtherSentOutsideAProbe() public { | |
| 267 | + deal(address(this), 1 ether); | |
| 268 | + | |
| 269 | + vm.expectRevert(WETHRegistry.NotProbing.selector); | |
| 270 | + payable(address(registry)).transfer(1 ether); | |
| 271 | + } | |
| 272 | + | |
| 273 | + function test_RevertWhen_EtherSentAfterRegistration() public { | |
| 274 | + IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY); | |
| 275 | + register(registry, weth); | |
| 276 | + deal(address(this), 1 ether); | |
| 277 | + | |
| 278 | + vm.expectRevert(WETHRegistry.NotProbing.selector); | |
| 279 | + payable(address(registry)).transfer(1 ether); | |
| 280 | + } | |
| 281 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,281 @@ | |||
| 1 | +// SPDX-License-Identifier: MIT | ||
| 2 | +pragma solidity ^0.8.30; | ||
| 3 | + | ||
| 4 | +import {IWETH} from "../src/IWETH.sol"; | ||
| 5 | +import {WETHRegistry} from "../src/WETHRegistry.sol"; | ||
| 6 | +import {Fixtures} from "./Fixtures.sol"; | ||
| 7 | +import {MockWETH} from "./mocks/MockWETH.sol"; | ||
| 8 | +import {Test} from "forge-std/Test.sol"; | ||
| 9 | + | ||
| 10 | +/// @notice A registrar with no payable fallback, which is what a deployer | ||
| 11 | +/// contract or a multisig looks like from the registry's side. | ||
| 12 | +contract PlainRegistrar { | ||
| 13 | + function register(WETHRegistry registry, IWETH weth) external payable { | ||
| 14 | + registry.register{value: msg.value}(weth); | ||
| 15 | + } | ||
| 16 | +} | ||
| 17 | + | ||
| 18 | +/// @title WETHRegistryTest | ||
| 19 | +/// @notice Covers the two gates the registry exists to run — is this code a | ||
| 20 | +/// reviewed wETH, and does it actually wrap — plus the write-once rule | ||
| 21 | +/// that stops the token under LOVE's peg from ever being swapped. | ||
| 22 | +contract WETHRegistryTest is Test, Fixtures { | ||
| 23 | + WETHRegistry registry; | ||
| 24 | + | ||
| 25 | + /// @dev Read once in `setUp`. Calling `registry.PROBE()` inline after | ||
| 26 | + /// `vm.expectRevert` would bind the expectation to that call instead | ||
| 27 | + /// of to `register`. | ||
| 28 | + uint256 probe; | ||
| 29 | + | ||
| 30 | + address constant WETH_AT = 0x4200000000000000000000000000000000000006; | ||
| 31 | + address constant OTHER_WETH_AT = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; | ||
| 32 | + | ||
| 33 | + event Registered(IWETH indexed weth, address indexed registrar, bytes32 codeHash); | ||
| 34 | + | ||
| 35 | + function setUp() public { | ||
| 36 | + registry = deployRegistry(); | ||
| 37 | + probe = registry.PROBE(); | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + /*////////////////////////////////////////////////////////////// | ||
| 41 | + THE ALLOWLIST | ||
| 42 | + //////////////////////////////////////////////////////////////*/ | ||
| 43 | + | ||
| 44 | + /// @dev The ten entries, reproducible from chain state with | ||
| 45 | + /// `script/weth-codehashes.sh`. Listing them here means a hash edited | ||
| 46 | + /// in the contract shows up as a failing test rather than as a silent | ||
| 47 | + /// change to what LOVE will accept. | ||
| 48 | + function test_KnownImplementations() public view { | ||
| 49 | + bytes32[10] memory allowed = [ | ||
| 50 | + // OP Stack legacy WETH9 | ||
| 51 | + bytes32(0x779bbf2a738ef09d961c945116197e2ac764c1b39304b2b4418cd4e42668b173), | ||
| 52 | + 0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c, | ||
| 53 | + 0x557c8e14d33f7cd67cad0141e1a49ebf3488a447fc3df7aa66b127778a0383d1, | ||
| 54 | + 0xf35fe602ba2a3b96f2e27ff7c8b8010800a8d0d616a5fb1f902e087b590355f3, | ||
| 55 | + // Canonical WETH9 | ||
| 56 | + 0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23, | ||
| 57 | + 0xa670ec6c272ddec6d328d6f3d5cad65a841a6ab45e8e5cf825150eb458be4f1f, | ||
| 58 | + 0x032e9cab14331328530468e54f1b91777b4d5c9dbbb400884badb32bc4113585, | ||
| 59 | + // OP Stack WETH, solc 0.8.15 | ||
| 60 | + 0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012, | ||
| 61 | + // Taiko | ||
| 62 | + 0x9f3d95086909fce850d997158aba31abe26c3aad6a413107ca0bf9d53a7c42e9, | ||
| 63 | + // Scroll | ||
| 64 | + 0xe8c4073351c26b9831c1e5af153b9be4713a4af9edfdf32b58077b735e120f14 | ||
| 65 | + ]; | ||
| 66 | + | ||
| 67 | + for (uint256 i = 0; i < allowed.length; i++) { | ||
| 68 | + assertTrue(registry.isKnownImplementation(allowed[i]), "allowlisted hash rejected"); | ||
| 69 | + } | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + function test_EmptyCodeHashIsNotKnown() public view { | ||
| 73 | + assertFalse(registry.isKnownImplementation(bytes32(0))); | ||
| 74 | + assertFalse(registry.isKnownImplementation(keccak256(""))); | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + function testFuzz_UnknownHashesAreRejected(bytes32 codeHash) public view { | ||
| 78 | + vm.assume(!registry.isKnownImplementation(codeHash)); | ||
| 79 | + assertFalse(registry.isKnownImplementation(codeHash)); | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + /*////////////////////////////////////////////////////////////// | ||
| 83 | + REGISTERING | ||
| 84 | + //////////////////////////////////////////////////////////////*/ | ||
| 85 | + | ||
| 86 | + function test_RegistersOpStackWeth() public { | ||
| 87 | + IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY); | ||
| 88 | + register(registry, weth); | ||
| 89 | + | ||
| 90 | + assertEq(address(registry.weth()), WETH_AT); | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | + function test_RegistersCanonicalWeth() public { | ||
| 94 | + IWETH weth = etchWeth(OTHER_WETH_AT, WETH9_CANONICAL); | ||
| 95 | + register(registry, weth); | ||
| 96 | + | ||
| 97 | + assertEq(address(registry.weth()), OTHER_WETH_AT); | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + function test_EmitsRegistered() public { | ||
| 101 | + IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY); | ||
| 102 | + deal(address(this), 1 ether); | ||
| 103 | + | ||
| 104 | + vm.expectEmit(true, true, true, true); | ||
| 105 | + emit Registered(weth, address(this), WETH_AT.codehash); | ||
| 106 | + registry.register{value: probe}(weth); | ||
| 107 | + } | ||
| 108 | + | ||
| 109 | + function test_NothingRegisteredInitially() public view { | ||
| 110 | + assertEq(address(registry.weth()), address(0)); | ||
| 111 | + } | ||
| 112 | + | ||
| 113 | + /// @dev The reason the probe wei is not refunded: paying it back means | ||
| 114 | + /// calling the registrar with value, which a contract without a | ||
| 115 | + /// payable fallback cannot accept. | ||
| 116 | + function test_RegistrarWithoutPayableFallbackCanRegister() public { | ||
| 117 | + IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY); | ||
| 118 | + PlainRegistrar registrar = new PlainRegistrar(); | ||
| 119 | + deal(address(this), 1 ether); | ||
| 120 | + | ||
| 121 | + registrar.register{value: probe}(registry, weth); | ||
| 122 | + | ||
| 123 | + assertEq(address(registry.weth()), WETH_AT); | ||
| 124 | + } | ||
| 125 | + | ||
| 126 | + function test_ProbeWeiStaysInTheRegistry() public { | ||
| 127 | + IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY); | ||
| 128 | + register(registry, weth); | ||
| 129 | + | ||
| 130 | + assertEq(address(registry).balance, probe); | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + /*////////////////////////////////////////////////////////////// | ||
| 134 | + THE BYTECODE GATE | ||
| 135 | + //////////////////////////////////////////////////////////////*/ | ||
| 136 | + | ||
| 137 | + /// @dev `MockWETH` wraps ether correctly and would sail through the | ||
| 138 | + /// behavioural probe. It is rejected on its code alone, which is the | ||
| 139 | + /// gate doing exactly what it is for: behaving well today says | ||
| 140 | + /// nothing about what else the code can do. | ||
| 141 | + function test_RevertWhen_CodeIsNotAReviewedImplementation() public { | ||
| 142 | + MockWETH mock = new MockWETH(); | ||
| 143 | + deal(address(this), 1 ether); | ||
| 144 | + | ||
| 145 | + vm.expectRevert( | ||
| 146 | + abi.encodeWithSelector( | ||
| 147 | + WETHRegistry.UnknownImplementation.selector, IWETH(address(mock)), address(mock).codehash | ||
| 148 | + ) | ||
| 149 | + ); | ||
| 150 | + registry.register{value: probe}(IWETH(address(mock))); | ||
| 151 | + } | ||
| 152 | + | ||
| 153 | + function test_RevertWhen_CandidateHasNoCode() public { | ||
| 154 | + address empty = makeAddr("empty"); | ||
| 155 | + deal(address(this), 1 ether); | ||
| 156 | + | ||
| 157 | + vm.expectRevert(abi.encodeWithSelector(WETHRegistry.UnknownImplementation.selector, IWETH(empty), bytes32(0))); | ||
| 158 | + registry.register{value: probe}(IWETH(empty)); | ||
| 159 | + } | ||
| 160 | + | ||
| 161 | + /*////////////////////////////////////////////////////////////// | ||
| 162 | + THE BEHAVIOURAL PROBE | ||
| 163 | + //////////////////////////////////////////////////////////////*/ | ||
| 164 | + | ||
| 165 | + /// @dev Mocking `balanceOf` leaves the codehash untouched, so the | ||
| 166 | + /// candidate still passes the allowlist and the probe is what has to | ||
| 167 | + /// catch it. A wrapper that mints nothing on deposit fails here. | ||
| 168 | + function test_RevertWhen_DepositMintsNothing() public { | ||
| 169 | + IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY); | ||
| 170 | + deal(address(this), 1 ether); | ||
| 171 | + | ||
| 172 | + vm.mockCall(WETH_AT, abi.encodeCall(weth.balanceOf, (address(registry))), abi.encode(uint256(0))); | ||
| 173 | + | ||
| 174 | + vm.expectRevert(abi.encodeWithSelector(WETHRegistry.DepositMismatch.selector, 1, 0)); | ||
| 175 | + registry.register{value: probe}(weth); | ||
| 176 | + } | ||
| 177 | + | ||
| 178 | + /// @dev Mints on deposit, but burns nothing on withdraw: the balance is | ||
| 179 | + /// read three times, and the third answer is the one that lies. | ||
| 180 | + function test_RevertWhen_WithdrawBurnsNothing() public { | ||
| 181 | + IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY); | ||
| 182 | + deal(address(this), 1 ether); | ||
| 183 | + | ||
| 184 | + bytes[] memory balances = new bytes[](3); | ||
| 185 | + balances[0] = abi.encode(uint256(0)); // before | ||
| 186 | + balances[1] = abi.encode(uint256(1)); // after deposit, correct | ||
| 187 | + balances[2] = abi.encode(uint256(1)); // after withdraw, should be 0 | ||
| 188 | + vm.mockCalls(WETH_AT, abi.encodeCall(weth.balanceOf, (address(registry))), balances); | ||
| 189 | + | ||
| 190 | + vm.expectRevert(abi.encodeWithSelector(WETHRegistry.WithdrawMismatch.selector, 0, 1)); | ||
| 191 | + registry.register{value: probe}(weth); | ||
| 192 | + } | ||
| 193 | + | ||
| 194 | + /// @dev Books the withdrawal correctly but keeps the ether. The balance | ||
| 195 | + /// check after the round trip is the only thing that notices. | ||
| 196 | + function test_RevertWhen_WithdrawKeepsTheEther() public { | ||
| 197 | + IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY); | ||
| 198 | + deal(address(this), 1 ether); | ||
| 199 | + | ||
| 200 | + vm.mockCall(WETH_AT, abi.encodeCall(weth.withdraw, (1)), ""); | ||
| 201 | + | ||
| 202 | + bytes[] memory balances = new bytes[](3); | ||
| 203 | + balances[0] = abi.encode(uint256(0)); | ||
| 204 | + balances[1] = abi.encode(uint256(1)); | ||
| 205 | + balances[2] = abi.encode(uint256(0)); | ||
| 206 | + vm.mockCalls(WETH_AT, abi.encodeCall(weth.balanceOf, (address(registry))), balances); | ||
| 207 | + | ||
| 208 | + vm.expectRevert(abi.encodeWithSelector(WETHRegistry.EtherNotReturned.selector, 1, 0)); | ||
| 209 | + registry.register{value: probe}(weth); | ||
| 210 | + } | ||
| 211 | + | ||
| 212 | + /*////////////////////////////////////////////////////////////// | ||
| 213 | + THE PROBE FEE | ||
| 214 | + //////////////////////////////////////////////////////////////*/ | ||
| 215 | + | ||
| 216 | + function test_RevertWhen_NoProbeValue() public { | ||
| 217 | + IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY); | ||
| 218 | + | ||
| 219 | + vm.expectRevert(abi.encodeWithSelector(WETHRegistry.ProbeValueRequired.selector, 0)); | ||
| 220 | + registry.register(weth); | ||
| 221 | + } | ||
| 222 | + | ||
| 223 | + function testFuzz_RevertWhen_WrongProbeValue(uint96 value) public { | ||
| 224 | + vm.assume(value != probe); | ||
| 225 | + IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY); | ||
| 226 | + deal(address(this), uint256(value) + 1 ether); | ||
| 227 | + | ||
| 228 | + vm.expectRevert(abi.encodeWithSelector(WETHRegistry.ProbeValueRequired.selector, value)); | ||
| 229 | + registry.register{value: value}(weth); | ||
| 230 | + } | ||
| 231 | + | ||
| 232 | + /*////////////////////////////////////////////////////////////// | ||
| 233 | + WRITE-ONCE | ||
| 234 | + //////////////////////////////////////////////////////////////*/ | ||
| 235 | + | ||
| 236 | + function test_RevertWhen_AlreadyRegistered() public { | ||
| 237 | + IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY); | ||
| 238 | + register(registry, weth); | ||
| 239 | + | ||
| 240 | + deal(address(this), 1 ether); | ||
| 241 | + vm.expectRevert(abi.encodeWithSelector(WETHRegistry.AlreadyRegistered.selector, weth)); | ||
| 242 | + registry.register{value: probe}(weth); | ||
| 243 | + } | ||
| 244 | + | ||
| 245 | + /// @dev Including with a second, equally valid wETH — the first answer is | ||
| 246 | + /// final, so nothing can move the token under a live peg. | ||
| 247 | + function test_RevertWhen_ReplacingWithAnotherValidWeth() public { | ||
| 248 | + IWETH first = etchWeth(WETH_AT, WETH9_OP_LEGACY); | ||
| 249 | + register(registry, first); | ||
| 250 | + | ||
| 251 | + IWETH second = etchWeth(OTHER_WETH_AT, WETH9_CANONICAL); | ||
| 252 | + deal(address(this), 1 ether); | ||
| 253 | + | ||
| 254 | + vm.expectRevert(abi.encodeWithSelector(WETHRegistry.AlreadyRegistered.selector, first)); | ||
| 255 | + registry.register{value: probe}(second); | ||
| 256 | + | ||
| 257 | + assertEq(address(registry.weth()), WETH_AT); | ||
| 258 | + } | ||
| 259 | + | ||
| 260 | + /*////////////////////////////////////////////////////////////// | ||
| 261 | + NOT A WALLET | ||
| 262 | + //////////////////////////////////////////////////////////////*/ | ||
| 263 | + | ||
| 264 | + /// @dev The registry has no owner and no way to move ether out, so it must | ||
| 265 | + /// not accept any outside a probe. | ||
| 266 | + function test_RevertWhen_EtherSentOutsideAProbe() public { | ||
| 267 | + deal(address(this), 1 ether); | ||
| 268 | + | ||
| 269 | + vm.expectRevert(WETHRegistry.NotProbing.selector); | ||
| 270 | + payable(address(registry)).transfer(1 ether); | ||
| 271 | + } | ||
| 272 | + | ||
| 273 | + function test_RevertWhen_EtherSentAfterRegistration() public { | ||
| 274 | + IWETH weth = etchWeth(WETH_AT, WETH9_OP_LEGACY); | ||
| 275 | + register(registry, weth); | ||
| 276 | + deal(address(this), 1 ether); | ||
| 277 | + | ||
| 278 | + vm.expectRevert(WETHRegistry.NotProbing.selector); | ||
| 279 | + payable(address(registry)).transfer(1 ether); | ||
| 280 | + } | ||
| 281 | +} | ||
added
test/fixtures/README.md +22 -0 | new file mode 100644 | ||
| @@ -0,0 +1,22 @@ | ||
| 1 | +# Fixtures | |
| 2 | + | |
| 3 | +Runtime bytecode of two wETH implementations on the allowlist, pulled straight | |
| 4 | +off mainnets so the tests exercise real code rather than a mock that resembles | |
| 5 | +it. `vm.etch` puts one of these at an address and its `EXTCODEHASH` is then, by | |
| 6 | +construction, the value `WETHRegistry` is looking for — which is the only way | |
| 7 | +to test the allowlist without weakening it for tests. | |
| 8 | + | |
| 9 | +| file | source | bytes | codehash | | |
| 10 | +| --- | --- | --- | --- | | |
| 11 | +| `weth9-op-legacy.hex` | Base, `0x4200000000000000000000000000000000000006` | 2041 | `0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c` | | |
| 12 | +| `weth9-canonical.hex` | Ethereum, `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` | 3124 | `0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23` | | |
| 13 | + | |
| 14 | +Refetch either with: | |
| 15 | + | |
| 16 | +```shell | |
| 17 | +cast code 0x4200000000000000000000000000000000000006 --rpc-url https://base-rpc.publicnode.com | |
| 18 | +``` | |
| 19 | + | |
| 20 | +Both hashes appear in `script/weth-codehashes.sh` output and in the allowlist in | |
| 21 | +`src/WETHRegistry.sol`, so a fixture that has drifted shows up as a test | |
| 22 | +failure rather than as a silent pass. | |
| new file mode 100644 | |||
| @@ -0,0 +1,22 @@ | |||
| 1 | +# Fixtures | ||
| 2 | + | ||
| 3 | +Runtime bytecode of two wETH implementations on the allowlist, pulled straight | ||
| 4 | +off mainnets so the tests exercise real code rather than a mock that resembles | ||
| 5 | +it. `vm.etch` puts one of these at an address and its `EXTCODEHASH` is then, by | ||
| 6 | +construction, the value `WETHRegistry` is looking for — which is the only way | ||
| 7 | +to test the allowlist without weakening it for tests. | ||
| 8 | + | ||
| 9 | +| file | source | bytes | codehash | | ||
| 10 | +| --- | --- | --- | --- | | ||
| 11 | +| `weth9-op-legacy.hex` | Base, `0x4200000000000000000000000000000000000006` | 2041 | `0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c` | | ||
| 12 | +| `weth9-canonical.hex` | Ethereum, `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` | 3124 | `0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23` | | ||
| 13 | + | ||
| 14 | +Refetch either with: | ||
| 15 | + | ||
| 16 | +```shell | ||
| 17 | +cast code 0x4200000000000000000000000000000000000006 --rpc-url https://base-rpc.publicnode.com | ||
| 18 | +``` | ||
| 19 | + | ||
| 20 | +Both hashes appear in `script/weth-codehashes.sh` output and in the allowlist in | ||
| 21 | +`src/WETHRegistry.sol`, so a fixture that has drifted shows up as a test | ||
| 22 | +failure rather than as a silent pass. | ||
added
test/fixtures/weth9-canonical.hex +1 -0 | new file mode 100644 | ||
| @@ -0,0 +1 @@ | ||
| 1 | +0x6060604052600436106100af576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306fdde03146100b9578063095ea7b31461014757806318160ddd146101a157806323b872dd146101ca5780632e1a7d4d14610243578063313ce5671461026657806370a082311461029557806395d89b41146102e2578063a9059cbb14610370578063d0e30db0146103ca578063dd62ed3e146103d4575b6100b7610440565b005b34156100c457600080fd5b6100cc6104dd565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561010c5780820151818401526020810190506100f1565b50505050905090810190601f1680156101395780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561015257600080fd5b610187600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061057b565b604051808215151515815260200191505060405180910390f35b34156101ac57600080fd5b6101b461066d565b6040518082815260200191505060405180910390f35b34156101d557600080fd5b610229600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061068c565b604051808215151515815260200191505060405180910390f35b341561024e57600080fd5b61026460048080359060200190919050506109d9565b005b341561027157600080fd5b610279610b05565b604051808260ff1660ff16815260200191505060405180910390f35b34156102a057600080fd5b6102cc600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610b18565b6040518082815260200191505060405180910390f35b34156102ed57600080fd5b6102f5610b30565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561033557808201518184015260208101905061031a565b50505050905090810190601f1680156103625780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561037b57600080fd5b6103b0600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091908035906020019091905050610bce565b604051808215151515815260200191505060405180910390f35b6103d2610440565b005b34156103df57600080fd5b61042a600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610be3565b6040518082815260200191505060405180910390f35b34600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055503373ffffffffffffffffffffffffffffffffffffffff167fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c346040518082815260200191505060405180910390a2565b60008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156105735780601f1061054857610100808354040283529160200191610573565b820191906000526020600020905b81548152906001019060200180831161055657829003601f168201915b505050505081565b600081600460003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040518082815260200191505060405180910390a36001905092915050565b60003073ffffffffffffffffffffffffffffffffffffffff1631905090565b600081600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054101515156106dc57600080fd5b3373ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff16141580156107b457507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205414155b156108cf5781600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020541015151561084457600080fd5b81600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055505b81600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254039250508190555081600360008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a3600190509392505050565b80600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410151515610a2757600080fd5b80600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f193505050501515610ab457600080fd5b3373ffffffffffffffffffffffffffffffffffffffff167f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65826040518082815260200191505060405180910390a250565b600260009054906101000a900460ff1681565b60036020528060005260406000206000915090505481565b60018054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610bc65780601f10610b9b57610100808354040283529160200191610bc6565b820191906000526020600020905b815481529060010190602001808311610ba957829003601f168201915b505050505081565b6000610bdb33848461068c565b905092915050565b60046020528160005260406000206020528060005260406000206000915091505054815600a165627a7a72305820deb4c2ccab3c2fdca32ab3f46728389c2fe2c165d5fafa07661e4e004f6c344a0029 | |
| new file mode 100644 | |||
| @@ -0,0 +1 @@ | |||
| 1 | +0x6060604052600436106100af576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306fdde03146100b9578063095ea7b31461014757806318160ddd146101a157806323b872dd146101ca5780632e1a7d4d14610243578063313ce5671461026657806370a082311461029557806395d89b41146102e2578063a9059cbb14610370578063d0e30db0146103ca578063dd62ed3e146103d4575b6100b7610440565b005b34156100c457600080fd5b6100cc6104dd565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561010c5780820151818401526020810190506100f1565b50505050905090810190601f1680156101395780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561015257600080fd5b610187600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061057b565b604051808215151515815260200191505060405180910390f35b34156101ac57600080fd5b6101b461066d565b6040518082815260200191505060405180910390f35b34156101d557600080fd5b610229600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061068c565b604051808215151515815260200191505060405180910390f35b341561024e57600080fd5b61026460048080359060200190919050506109d9565b005b341561027157600080fd5b610279610b05565b604051808260ff1660ff16815260200191505060405180910390f35b34156102a057600080fd5b6102cc600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610b18565b6040518082815260200191505060405180910390f35b34156102ed57600080fd5b6102f5610b30565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561033557808201518184015260208101905061031a565b50505050905090810190601f1680156103625780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561037b57600080fd5b6103b0600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091908035906020019091905050610bce565b604051808215151515815260200191505060405180910390f35b6103d2610440565b005b34156103df57600080fd5b61042a600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610be3565b6040518082815260200191505060405180910390f35b34600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055503373ffffffffffffffffffffffffffffffffffffffff167fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c346040518082815260200191505060405180910390a2565b60008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156105735780601f1061054857610100808354040283529160200191610573565b820191906000526020600020905b81548152906001019060200180831161055657829003601f168201915b505050505081565b600081600460003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040518082815260200191505060405180910390a36001905092915050565b60003073ffffffffffffffffffffffffffffffffffffffff1631905090565b600081600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054101515156106dc57600080fd5b3373ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff16141580156107b457507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205414155b156108cf5781600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020541015151561084457600080fd5b81600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055505b81600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254039250508190555081600360008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a3600190509392505050565b80600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410151515610a2757600080fd5b80600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f193505050501515610ab457600080fd5b3373ffffffffffffffffffffffffffffffffffffffff167f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65826040518082815260200191505060405180910390a250565b600260009054906101000a900460ff1681565b60036020528060005260406000206000915090505481565b60018054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610bc65780601f10610b9b57610100808354040283529160200191610bc6565b820191906000526020600020905b815481529060010190602001808311610ba957829003601f168201915b505050505081565b6000610bdb33848461068c565b905092915050565b60046020528160005260406000206020528060005260406000206000915091505054815600a165627a7a72305820deb4c2ccab3c2fdca32ab3f46728389c2fe2c165d5fafa07661e4e004f6c344a0029 | ||
added
test/fixtures/weth9-op-legacy.hex +1 -0 | new file mode 100644 | ||
| @@ -0,0 +1 @@ | ||
| 1 | +0x6080604052600436106100bc5760003560e01c8063313ce56711610074578063a9059cbb1161004e578063a9059cbb146102cb578063d0e30db0146100bc578063dd62ed3e14610311576100bc565b8063313ce5671461024b57806370a082311461027657806395d89b41146102b6576100bc565b806318160ddd116100a557806318160ddd146101aa57806323b872dd146101d15780632e1a7d4d14610221576100bc565b806306fdde03146100c6578063095ea7b314610150575b6100c4610359565b005b3480156100d257600080fd5b506100db6103a8565b6040805160208082528351818301528351919283929083019185019080838360005b838110156101155781810151838201526020016100fd565b50505050905090810190601f1680156101425780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561015c57600080fd5b506101966004803603604081101561017357600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135610454565b604080519115158252519081900360200190f35b3480156101b657600080fd5b506101bf6104c7565b60408051918252519081900360200190f35b3480156101dd57600080fd5b50610196600480360360608110156101f457600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135811691602081013590911690604001356104cb565b34801561022d57600080fd5b506100c46004803603602081101561024457600080fd5b503561066b565b34801561025757600080fd5b50610260610700565b6040805160ff9092168252519081900360200190f35b34801561028257600080fd5b506101bf6004803603602081101561029957600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16610709565b3480156102c257600080fd5b506100db61071b565b3480156102d757600080fd5b50610196600480360360408110156102ee57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135610793565b34801561031d57600080fd5b506101bf6004803603604081101561033457600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160200135166107a7565b33600081815260036020908152604091829020805434908101909155825190815291517fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c9281900390910190a2565b6000805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f8101849004840282018401909252818152929183018282801561044c5780601f106104215761010080835404028352916020019161044c565b820191906000526020600020905b81548152906001019060200180831161042f57829003601f168201915b505050505081565b33600081815260046020908152604080832073ffffffffffffffffffffffffffffffffffffffff8716808552908352818420869055815186815291519394909390927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925928290030190a350600192915050565b4790565b73ffffffffffffffffffffffffffffffffffffffff83166000908152600360205260408120548211156104fd57600080fd5b73ffffffffffffffffffffffffffffffffffffffff84163314801590610573575073ffffffffffffffffffffffffffffffffffffffff841660009081526004602090815260408083203384529091529020547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff14155b156105ed5773ffffffffffffffffffffffffffffffffffffffff841660009081526004602090815260408083203384529091529020548211156105b557600080fd5b73ffffffffffffffffffffffffffffffffffffffff841660009081526004602090815260408083203384529091529020805483900390555b73ffffffffffffffffffffffffffffffffffffffff808516600081815260036020908152604080832080548890039055938716808352918490208054870190558351868152935191937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef929081900390910190a35060019392505050565b3360009081526003602052604090205481111561068757600080fd5b33600081815260036020526040808220805485900390555183156108fc0291849190818181858888f193505050501580156106c6573d6000803e3d6000fd5b5060408051828152905133917f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65919081900360200190a250565b60025460ff1681565b60036020526000908152604090205481565b60018054604080516020600284861615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f8101849004840282018401909252818152929183018282801561044c5780601f106104215761010080835404028352916020019161044c565b60006107a03384846104cb565b9392505050565b60046020908152600092835260408084209091529082529020548156fea265627a7a72315820e496abb80c5983b030f680d0bd88f66bf44e261bc3be070d612dd72f9f1f5e9a64736f6c63430005110032 | |
| new file mode 100644 | |||
| @@ -0,0 +1 @@ | |||
| 1 | +0x6080604052600436106100bc5760003560e01c8063313ce56711610074578063a9059cbb1161004e578063a9059cbb146102cb578063d0e30db0146100bc578063dd62ed3e14610311576100bc565b8063313ce5671461024b57806370a082311461027657806395d89b41146102b6576100bc565b806318160ddd116100a557806318160ddd146101aa57806323b872dd146101d15780632e1a7d4d14610221576100bc565b806306fdde03146100c6578063095ea7b314610150575b6100c4610359565b005b3480156100d257600080fd5b506100db6103a8565b6040805160208082528351818301528351919283929083019185019080838360005b838110156101155781810151838201526020016100fd565b50505050905090810190601f1680156101425780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561015c57600080fd5b506101966004803603604081101561017357600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135610454565b604080519115158252519081900360200190f35b3480156101b657600080fd5b506101bf6104c7565b60408051918252519081900360200190f35b3480156101dd57600080fd5b50610196600480360360608110156101f457600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135811691602081013590911690604001356104cb565b34801561022d57600080fd5b506100c46004803603602081101561024457600080fd5b503561066b565b34801561025757600080fd5b50610260610700565b6040805160ff9092168252519081900360200190f35b34801561028257600080fd5b506101bf6004803603602081101561029957600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16610709565b3480156102c257600080fd5b506100db61071b565b3480156102d757600080fd5b50610196600480360360408110156102ee57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135610793565b34801561031d57600080fd5b506101bf6004803603604081101561033457600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160200135166107a7565b33600081815260036020908152604091829020805434908101909155825190815291517fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c9281900390910190a2565b6000805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f8101849004840282018401909252818152929183018282801561044c5780601f106104215761010080835404028352916020019161044c565b820191906000526020600020905b81548152906001019060200180831161042f57829003601f168201915b505050505081565b33600081815260046020908152604080832073ffffffffffffffffffffffffffffffffffffffff8716808552908352818420869055815186815291519394909390927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925928290030190a350600192915050565b4790565b73ffffffffffffffffffffffffffffffffffffffff83166000908152600360205260408120548211156104fd57600080fd5b73ffffffffffffffffffffffffffffffffffffffff84163314801590610573575073ffffffffffffffffffffffffffffffffffffffff841660009081526004602090815260408083203384529091529020547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff14155b156105ed5773ffffffffffffffffffffffffffffffffffffffff841660009081526004602090815260408083203384529091529020548211156105b557600080fd5b73ffffffffffffffffffffffffffffffffffffffff841660009081526004602090815260408083203384529091529020805483900390555b73ffffffffffffffffffffffffffffffffffffffff808516600081815260036020908152604080832080548890039055938716808352918490208054870190558351868152935191937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef929081900390910190a35060019392505050565b3360009081526003602052604090205481111561068757600080fd5b33600081815260036020526040808220805485900390555183156108fc0291849190818181858888f193505050501580156106c6573d6000803e3d6000fd5b5060408051828152905133917f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65919081900360200190a250565b60025460ff1681565b60036020526000908152604090205481565b60018054604080516020600284861615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f8101849004840282018401909252818152929183018282801561044c5780601f106104215761010080835404028352916020019161044c565b60006107a03384846104cb565b9392505050565b60046020908152600092835260408084209091529082529020548156fea265627a7a72315820e496abb80c5983b030f680d0bd88f66bf44e261bc3be070d612dd72f9f1f5e9a64736f6c63430005110032 | ||