From 5adc7c48fdc77fff82735adbeadbb2554d1d37c6 Mon Sep 17 00:00:00 2001 From: David Meister Date: Fri, 14 Aug 2026 16:00:32 +0000 Subject: [PATCH 1/3] docs: say what findDeployBlock, isStartBlock and the deploy actually do MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four of the five doc-vs-code claims in #18 that current main still carries. findDeployBlock's NatSpec claimed the result is validated via isStartBlock. It is not, and adding the call would not be a check: the loop exits with the code hash matching at the result and not matching at the block before it whatever the history, so isStartBlock holds of the result by construction and could only agree. Dropped, and the requirement that is real is documented instead — both functions read the code hash as if it were monotone over the search range, and a target that held the hash, lost it and holds it again defeats them silently. The Zoltu factory derives with CREATE2 under a zero salt; a test comment still said the factory's nonce predicted the address. CLAUDE.md described dependency checking as an all-network pre-flight, when it is per network and only on the branch that deploys. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 15 +++++++++++---- src/lib/LibRainDeploy.sol | 25 +++++++++++++++++++++++-- test/src/lib/LibRainDeploy.t.sol | 3 ++- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b67a857..47a4bad 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -92,8 +92,10 @@ These are referenced in `foundry.toml` under `[rpc_endpoints]`. is the only point at which such a check means anything: registry bindings are mutable, so a pre-deploy check would read a source that can change before the constructor that consumes it -- `deployToNetworks(...)` — forks each network, verifies the factory and - dependencies, deploys via Zoltu, verifies address and code hash +- `deployToNetworks(...)` — forks each network; where the expected address is + still empty it verifies the factory and dependencies on that fork, deploys via + Zoltu and checks the address; the code hash is checked on every network, + deployed here or already there - `deployAndBroadcast(...)` — the main entry point: derives the deployer from a private key, then `deployToNetworks` @@ -292,8 +294,13 @@ expected addresses, expected code hashes, and dependency lists. - **Code hash verification**: Post-deploy bytecode integrity is verified against `expectedCodeHash`. The address registry is verified the same way before it is read. -- **Dependency checking**: Before deploying to any network, all dependencies - (contract addresses) are verified to have code on-chain. +- **Dependency checking**: per network, and only where the deploy actually runs. + On a network with no code at `expectedAddress`, the Zoltu factory must have + code and match `ZOLTU_FACTORY_CODEHASH` and every dependency must have code, + all read on that network's own fork immediately before broadcasting. A network + that already has the code skips the deploy and the dependency check with it: a + contract that is already deployed does not need its dependencies present to + stay deployed. There is no all-network pre-flight. - **Idempotent deploys**: If code already exists at the expected address, deployment is skipped for that network. - **Resolve once, verify after**: registry bindings are mutable, so the diff --git a/src/lib/LibRainDeploy.sol b/src/lib/LibRainDeploy.sol index f5769b5..6e46488 100644 --- a/src/lib/LibRainDeploy.sol +++ b/src/lib/LibRainDeploy.sol @@ -81,6 +81,15 @@ library LibRainDeploy { /// hash at `blockNumber` and does NOT have it at `blockNumber - 1`. At /// block 0, only the first condition is checked. The fork is restored to /// its original block number after checking. + /// + /// "First" is only true of a target whose code hash is MONOTONE: once it + /// equals `expectedCodeHash` at some block it equals it at every later + /// block. This reads two adjacent blocks and nothing else, so on a target + /// that held `expectedCodeHash`, lost it and holds it again — a pre-Cancun + /// `SELFDESTRUCT` followed by a `CREATE2` redeploy of the same code at the + /// same address, say — it answers true at EVERY block where the hash + /// reappears, not only the earliest. Monotonicity is the caller's to know: + /// two blocks cannot show it. /// @param vm The Vm instance for fork manipulation. /// @param target The contract address to check. /// @param expectedCodeHash The code hash to look for. @@ -104,8 +113,20 @@ library LibRainDeploy { /// searching the fork history. Requires an active fork with archive access /// back to `startBlock`. The fork is restored to its original block /// number before returning. The target's code hash is verified against the - /// expected value before searching. The result is validated via - /// `isStartBlock`. + /// expected value before searching. + /// + /// The search REQUIRES the target's code hash to be monotone over + /// `[startBlock, block.number]`, in the sense `isStartBlock` describes. + /// Against a target that held `expectedCodeHash`, lost it and holds it + /// again, the search converges on one of those appearances with no way to + /// say which, and the result is meaningless as the subgraph start block it + /// is typically used as. + /// + /// That is the caller's precondition because nothing here can check it, and + /// `isStartBlock` least of all: the loop exits with the code hash matching + /// at the result and not matching at the block before it whatever the + /// history, so `isStartBlock` holds of the result by construction and + /// running it here could only ever agree. /// @param vm The Vm instance for fork manipulation. /// @param target The contract address to search for. /// @param expectedCodeHash The expected code hash of the target contract. diff --git a/test/src/lib/LibRainDeploy.t.sol b/test/src/lib/LibRainDeploy.t.sol index cd8bce4..f5698a9 100644 --- a/test/src/lib/LibRainDeploy.t.sol +++ b/test/src/lib/LibRainDeploy.t.sol @@ -358,7 +358,8 @@ contract LibRainDeployTest is Test { } /// `deployZoltu` MUST deploy a contract via the Zoltu factory and return - /// the deterministic address predicted by the factory's nonce. + /// the deterministic address the factory derives with `CREATE2` over the + /// creation code under a zero salt. function testDeployZoltu() external { vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); address deployed = this.externalDeployZoltu(type(MockDeployable).creationCode); From 5aa27e2f2a5b3439cdc5a3c747ba37174754b313 Mon Sep 17 00:00:00 2001 From: David Meister Date: Fri, 14 Aug 2026 16:04:34 +0000 Subject: [PATCH 2/3] docs: give the invariant, so the reader can check the tautology claim Co-Authored-By: Claude Opus 5 (1M context) --- src/lib/LibRainDeploy.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/lib/LibRainDeploy.sol b/src/lib/LibRainDeploy.sol index c1115ac..8c0de6e 100644 --- a/src/lib/LibRainDeploy.sol +++ b/src/lib/LibRainDeploy.sol @@ -125,10 +125,12 @@ library LibRainDeploy { /// is typically used as. /// /// That is the caller's precondition because nothing here can check it, and - /// `isStartBlock` least of all: the loop exits with the code hash matching - /// at the result and not matching at the block before it whatever the - /// history, so `isStartBlock` holds of the result by construction and - /// running it here could only ever agree. + /// `isStartBlock` least of all. `high` is only ever a block where the code + /// hash matches and `low` is only ever one past a block where it does not, + /// so where they meet the hash matches and did not match at the block + /// before — `isStartBlock`'s exact condition, satisfied by construction and + /// satisfied on a non-monotone history just the same. Running it on the + /// result would read two more archive blocks to agree with itself. /// @param vm The Vm instance for fork manipulation. /// @param target The contract address to search for. /// @param expectedCodeHash The expected code hash of the target contract. From 76307497c8999f46c72da5b09c1f8053a3e8f2ed Mon Sep 17 00:00:00 2001 From: David Meister Date: Fri, 14 Aug 2026 16:30:54 +0000 Subject: [PATCH 3/3] docs: state the loop invariant so it holds at the initial values too Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 9 +++++++-- src/lib/LibRainDeploy.sol | 15 ++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 47a4bad..13a2c0b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -82,8 +82,13 @@ These are referenced in `foundry.toml` under `[rpc_endpoints]`. returns the deployed address - `supportedNetworks()` — returns the list of Rain-supported network names (used as foundry RPC config aliases) -- `isStartBlock(...)` / `findDeployBlock(...)` — binary search a fork's history - for the block a contract first appears at +- `isStartBlock(...)` — reads two adjacent blocks: true when the target has the + expected code hash at a block and does not have it at the block before +- `findDeployBlock(...)` — binary searches a fork's history for a block where + the target has the expected code hash and did not have it at the block before. + Either one is "the block a contract first appears at" only where that code + hash is monotone; against a target that held the hash, lost it and holds it + again both answer about an appearance neither can identify - `checkResolvedAddresses(...)` — asserts an already-deployed contract holds the addresses the deployment expected, on the currently selected fork, via consumer-supplied static reads diff --git a/src/lib/LibRainDeploy.sol b/src/lib/LibRainDeploy.sol index 8c0de6e..616ec9f 100644 --- a/src/lib/LibRainDeploy.sol +++ b/src/lib/LibRainDeploy.sol @@ -126,11 +126,16 @@ library LibRainDeploy { /// /// That is the caller's precondition because nothing here can check it, and /// `isStartBlock` least of all. `high` is only ever a block where the code - /// hash matches and `low` is only ever one past a block where it does not, - /// so where they meet the hash matches and did not match at the block - /// before — `isStartBlock`'s exact condition, satisfied by construction and - /// satisfied on a non-monotone history just the same. Running it on the - /// result would read two more archive blocks to agree with itself. + /// hash matches: it starts at the current block, checked above, and + /// otherwise takes a `mid` the loop has just read as matching. `low` is + /// either `startBlock`, checked above as NOT matching, or one past a `mid` + /// the loop has just read as not matching. It cannot still be `startBlock` + /// when the loop ends, because that needs `high` down at `startBlock` and + /// `high` is only ever a matching block, so where they meet the hash + /// matches and did not match at the block before — `isStartBlock`'s exact + /// condition, satisfied by construction and satisfied on a non-monotone + /// history just the same. Running it on the result would read two more + /// archive blocks to agree with itself. /// @param vm The Vm instance for fork manipulation. /// @param target The contract address to search for. /// @param expectedCodeHash The expected code hash of the target contract.