From 432f74d360955d8fec3df9a559bb76dae8d41aa6 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Thu, 5 Mar 2026 18:34:40 +0400 Subject: [PATCH 1/9] add CLAUDE.md Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5735476 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,55 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +rain.deploy is a Solidity library for deploying Rain Protocol contracts via the Zoltu deterministic deployment proxy to multiple EVM networks. It ensures identical contract addresses across all supported chains (Arbitrum, Base, Flare, Polygon) by using CREATE2-style deterministic deployments. + +## Build & Development + +This project uses **Foundry** (forge) for Solidity development and **Nix** for environment management. + +```bash +# Enter the nix dev shell (provides forge and all tooling) +nix develop + +# Build +nix develop -c forge build + +# Run tests +nix develop -c rainix-sol-test + +# Run a single test +nix develop -c forge test --match-test "testName" + +# Static analysis / linting +nix develop -c rainix-sol-static + +# License/legal checks (REUSE compliance) +nix develop -c rainix-sol-legal +``` + +CI runs three matrix tasks: `rainix-sol-legal`, `rainix-sol-test`, `rainix-sol-static`. + +## Architecture + +The entire library is a single file: `src/lib/LibRainDeploy.sol`. + +**LibRainDeploy** provides: +- `deployZoltu(bytes creationCode)` — deploys creation code via the Zoltu factory (`0x7A0D94F55792C434d74a40883C6ed8545E406D12`) using low-level `call`, returns the deployed address +- `supportedNetworks()` — returns the list of Rain-supported network names (used as foundry RPC config aliases) +- `deployAndBroadcastToSupportedNetworks(...)` — the main entry point: forks each network, checks dependencies exist, deploys via Zoltu, verifies the deployed address and code hash match expectations + +The library is designed to be called from Foundry scripts (`forge script`) in consuming repos, not directly. Consuming repos provide their own creation code, expected addresses, expected code hashes, and dependency lists. + +## Key Design Patterns + +- **Deterministic addresses**: Zoltu proxy ensures same address on every chain. Deployments fail if the resulting address doesn't match `expectedAddress`. +- **Code hash verification**: Post-deploy bytecode integrity is verified against `expectedCodeHash`. +- **Dependency checking**: Before deploying to any network, all dependencies (contract addresses) are verified to have code on-chain. +- **Idempotent deploys**: If code already exists at the expected address, deployment is skipped for that network. + +## License + +DecentraLicense 1.0 (LicenseRef-DCL-1.0). All source files must have SPDX headers. REUSE compliance is enforced in CI. From 9e6489eee1d6b8c96f81a4e1ba708defcc2cf8ca Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Thu, 5 Mar 2026 19:59:19 +0400 Subject: [PATCH 2/9] audit fixes: TOCTOU mitigation, tests, NatSpec, rename - Split monolithic deploy function into checkDependencies, deployToNetworks, and deployAndBroadcast for TOCTOU mitigation via codehash tracking - Add NoNetworks error for empty networks array - Add DependencyChanged error for codehash/code-length changes between phases - Add comprehensive test suite (11 tests) covering all functions and error paths - Fix NatSpec: add missing @param tags, correct description and @return docs - Rename deployAndBroadcastToSupportedNetworks to deployAndBroadcast - Fix typo "verficiation" -> "verification" - Add foundry.toml with cbor_metadata=false for stable bytecode - Add .env to .gitignore Co-Authored-By: Claude Opus 4.6 --- .gitignore | 4 +- CLAUDE.md | 2 +- audit/2026-03-05-01/pass0/process.md | 32 +++ audit/2026-03-05-01/pass1/LibRainDeploy.md | 72 ++++++ audit/2026-03-05-01/pass2/LibRainDeploy.md | 65 ++++++ audit/2026-03-05-01/pass3/LibRainDeploy.md | 109 +++++++++ audit/2026-03-05-01/pass4/LibRainDeploy.md | 102 +++++++++ audit/2026-03-05-01/pass5/LibRainDeploy.md | 108 +++++++++ audit/2026-03-05-01/triage.md | 52 +++++ foundry.toml | 13 ++ src/lib/LibRainDeploy.sol | 108 +++++++-- test/src/lib/LibRainDeploy.t.sol | 244 +++++++++++++++++++++ 12 files changed, 887 insertions(+), 24 deletions(-) create mode 100644 audit/2026-03-05-01/pass0/process.md create mode 100644 audit/2026-03-05-01/pass1/LibRainDeploy.md create mode 100644 audit/2026-03-05-01/pass2/LibRainDeploy.md create mode 100644 audit/2026-03-05-01/pass3/LibRainDeploy.md create mode 100644 audit/2026-03-05-01/pass4/LibRainDeploy.md create mode 100644 audit/2026-03-05-01/pass5/LibRainDeploy.md create mode 100644 audit/2026-03-05-01/triage.md create mode 100644 foundry.toml create mode 100644 test/src/lib/LibRainDeploy.t.sol diff --git a/.gitignore b/.gitignore index 668aefc..8a6dbfd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ cache -out \ No newline at end of file +out +.fixes +.env \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 5735476..b786ca0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -rain.deploy is a Solidity library for deploying Rain Protocol contracts via the Zoltu deterministic deployment proxy to multiple EVM networks. It ensures identical contract addresses across all supported chains (Arbitrum, Base, Flare, Polygon) by using CREATE2-style deterministic deployments. +rain.deploy is a Solidity library for deploying Rain Protocol contracts via the Zoltu deterministic deployment proxy to multiple EVM networks. It ensures identical contract addresses across all supported chains (Arbitrum, Base, Flare, Polygon) because the Zoltu proxy is deployed at the same address on every chain and uses CREATE with a predictable nonce. ## Build & Development diff --git a/audit/2026-03-05-01/pass0/process.md b/audit/2026-03-05-01/pass0/process.md new file mode 100644 index 0000000..9af0a63 --- /dev/null +++ b/audit/2026-03-05-01/pass0/process.md @@ -0,0 +1,32 @@ +# Pass 0: Process Review + +## Documents Reviewed + +- `CLAUDE.md` (56 lines) +- `README.md` (32 lines) +- `flake.nix` (19 lines) +- `.github/workflows/rainix.yaml` (57 lines) +- `REUSE.toml` (17 lines) +- `.gitignore` (3 lines) + +## Findings + +### A01-1 [LOW] Missing foundry.toml + +`REUSE.toml` lists `foundry.toml` in its annotation paths (line 14), but no `foundry.toml` exists at the project root. This means either: +- The project relies on a foundry.toml provided by the rainix nix devshell at runtime, which is not documented +- The REUSE.toml annotation is stale + +A future session trying to customize forge settings (e.g., remappings, optimizer settings, solc version) would not know where configuration lives or whether it's expected to exist. + +### A01-2 [LOW] CLAUDE.md describes Zoltu as "CREATE2-style" but it is not CREATE2 + +CLAUDE.md line 7 says "by using CREATE2-style deterministic deployments." The Zoltu proxy uses `CREATE` (not `CREATE2`) — the determinism comes from the proxy's nonce being predictable (nonce 1 on first use). This could mislead a future session into reasoning about CREATE2 salt mechanics that don't apply here. + +### A01-3 [INFO] CI environment variables not documented in CLAUDE.md + +The CI workflow references several env vars (`ETH_RPC_URL`, `DEPLOYMENT_KEY`, `DEPLOY_BROADCAST`, `DEPLOY_VERIFIER`, `DEPLOY_METABOARD_ADDRESS`, etc.) that are not mentioned in CLAUDE.md. A future session attempting to run deployment scripts locally would not know what variables are expected. + +### A01-4 [INFO] No foundry.toml means no documented solc version or optimizer settings + +Without a `foundry.toml` in the repo, there's no record of which Solidity compiler version or optimizer settings are used. The pragma in the source is `^0.8.25` which allows any 0.8.x >= 0.8.25. The actual compiler version used depends on whatever the nix devshell provides, which is opaque to someone reading only this repo. diff --git a/audit/2026-03-05-01/pass1/LibRainDeploy.md b/audit/2026-03-05-01/pass1/LibRainDeploy.md new file mode 100644 index 0000000..b43ffd1 --- /dev/null +++ b/audit/2026-03-05-01/pass1/LibRainDeploy.md @@ -0,0 +1,72 @@ +# Pass 1: Security -- LibRainDeploy.sol + +**Agent:** A01 +**File:** `src/lib/LibRainDeploy.sol` (148 lines) + +## Evidence of Thorough Reading + +**Library name:** `LibRainDeploy` (line 14) + +**Functions:** +| Function | Line | Visibility | +|----------|------|------------| +| `deployZoltu(bytes memory creationCode)` | 48 | internal | +| `supportedNetworks()` | 67 | internal pure | +| `deployAndBroadcastToSupportedNetworks(Vm, string[], uint256, bytes, string, address, bytes32, address[])` | 83 | internal | + +**Errors:** +| Error | Line | +|-------|------| +| `DeployFailed(bool success, address deployedAddress)` | 18 | +| `MissingDependency(string network, address dependency)` | 21 | +| `UnexpectedDeployedAddress(address expected, address actual)` | 24 | +| `UnexpectedDeployedCodeHash(bytes32 expected, bytes32 actual)` | 27 | + +**Constants:** +| Constant | Line | Value | +|----------|------|-------| +| `ZOLTU_FACTORY` | 30 | `0x7A0D94F55792C434d74a40883C6ed8545E406D12` | +| `ARBITRUM_ONE` | 33 | `"arbitrum"` | +| `BASE` | 36 | `"base"` | +| `FLARE` | 39 | `"flare"` | +| `POLYGON` | 42 | `"polygon"` | + +**Types:** None defined. + +**Imports:** `Vm` from `forge-std/Vm.sol` (line 5), `console2` from `forge-std/console2.sol` (line 6). + +## Findings + +### A01-1 [LOW] Silent `address(0)` return when `networks` array is empty + +**Location:** `deployAndBroadcastToSupportedNetworks`, lines 92-147 + +**Description:** +If the `networks` parameter is an empty array, both for-loops (lines 98-115 and 118-146) are skipped entirely. The return variable `deployedAddress` is never assigned and defaults to `address(0)`. The function returns successfully with `address(0)`, with no error or revert. + +A caller that accidentally passes an empty `networks` array -- for example by constructing it dynamically and encountering a filtering bug -- would receive `address(0)` as a valid deployed address with no indication of failure. + +**Impact:** A script relying on the return value would proceed with `address(0)`, which could lead to downstream misconfiguration (e.g., setting `address(0)` as a trusted contract address in further deployment steps). + +### A01-2 [LOW] TOCTOU gap between dependency checking and deployment + +**Location:** `deployAndBroadcastToSupportedNetworks`, lines 98-146 + +**Description:** +The function performs dependency checking in a first loop (lines 98-115) by creating a fork for each network and verifying that the Zoltu factory and all dependencies have code. It then performs deployment in a second loop (lines 118-146) by creating a *new* fork for each network via `vm.createSelectFork`. + +Each `createSelectFork` call fetches the chain state at the current block. Between the dependency-check fork and the deploy fork for a given network, the underlying chain advances. If a dependency contract were destroyed (via `SELFDESTRUCT` / `SELFDESTRUCT` scheduled in a prior transaction) in the intervening blocks, the deployment would proceed without re-checking, potentially deploying a contract whose constructor references a now-nonexistent dependency. + +**Mitigating factors:** +- This runs as a Foundry script, not on-chain, so the window is small (seconds at most) +- `SELFDESTRUCT` of well-established dependency contracts is highly unlikely in practice +- The deployment itself would likely fail or produce wrong code, caught by the `expectedCodeHash` check on line 135 + +**Impact:** In a contrived scenario, a deployment could succeed on a network where a dependency has been removed between the check and deploy forks. Practically very unlikely but the two-loop pattern introduces an unnecessary gap. + +### A01-3 [INFO] Typo in log message + +**Location:** Line 140 + +**Description:** +The log message reads `"manual verficiation command:"` -- "verficiation" should be "verification". This is cosmetic and has no security impact, but could cause confusion when searching logs by keyword. diff --git a/audit/2026-03-05-01/pass2/LibRainDeploy.md b/audit/2026-03-05-01/pass2/LibRainDeploy.md new file mode 100644 index 0000000..1fae2ed --- /dev/null +++ b/audit/2026-03-05-01/pass2/LibRainDeploy.md @@ -0,0 +1,65 @@ +# Pass 2: Test Coverage -- LibRainDeploy.sol + +**Agent:** A01 +**File:** `src/lib/LibRainDeploy.sol` (148 lines) + +## Evidence of Thorough Reading + +**Library name:** `LibRainDeploy` (line 14) + +**Functions:** +| Function | Line | Visibility | +|----------|------|------------| +| `deployZoltu(bytes memory creationCode)` | 48 | internal | +| `supportedNetworks()` | 67 | internal pure | +| `deployAndBroadcastToSupportedNetworks(Vm, string[], uint256, bytes, string, address, bytes32, address[])` | 83 | internal | + +**Errors:** +| Error | Line | +|-------|------| +| `DeployFailed(bool success, address deployedAddress)` | 18 | +| `MissingDependency(string network, address dependency)` | 21 | +| `UnexpectedDeployedAddress(address expected, address actual)` | 24 | +| `UnexpectedDeployedCodeHash(bytes32 expected, bytes32 actual)` | 27 | + +**Constants:** +| Constant | Line | +|----------|------| +| `ZOLTU_FACTORY` | 30 | +| `ARBITRUM_ONE` | 33 | +| `BASE` | 36 | +| `FLARE` | 39 | +| `POLYGON` | 42 | + +## Test File Search + +- `test/` directory: does not exist +- Grep for `LibRainDeploy` across all `.sol` files: only found in `src/lib/LibRainDeploy.sol` itself +- No test files exist anywhere in the project (excluding `lib/forge-std/test/`) + +## Findings + +### A01-1 [HIGH] No test file exists for LibRainDeploy.sol + +**Description:** +The sole source file in this project has zero test coverage. There is no `test/` directory and no test files anywhere in the repo (outside of the forge-std submodule). + +Functions with no test coverage: +- `deployZoltu` — the core deployment mechanism using inline assembly +- `supportedNetworks` — pure function returning network list +- `deployAndBroadcastToSupportedNetworks` — the main orchestration function + +Error paths with no test coverage: +- `DeployFailed` revert (line 61) — when Zoltu factory call fails or returns zero address +- `MissingDependency` revert (lines 106, 112) — when Zoltu factory or dependencies missing on a network +- `UnexpectedDeployedAddress` revert (line 132) — when deployed address doesn't match expected +- `UnexpectedDeployedCodeHash` revert (line 136) — when code hash doesn't match expected + +Edge cases with no coverage: +- Empty `networks` array (returns `address(0)` silently — see Pass 1 A01-1) +- Empty `dependencies` array (valid but untested) +- Empty `creationCode` (behavior undefined) +- `creationCode` that reverts during construction +- Re-deployment when code already exists at expected address (skip path, line 127) + +Note: Testing this library is non-trivial because it relies on Foundry VM cheatcodes (`createSelectFork`, `startBroadcast`, `rememberKey`) and external network state. However, Foundry supports forked-mode testing which can exercise these paths. diff --git a/audit/2026-03-05-01/pass3/LibRainDeploy.md b/audit/2026-03-05-01/pass3/LibRainDeploy.md new file mode 100644 index 0000000..c941d3f --- /dev/null +++ b/audit/2026-03-05-01/pass3/LibRainDeploy.md @@ -0,0 +1,109 @@ +# Pass 3: Documentation -- LibRainDeploy.sol + +**Agent:** A01 +**File:** `src/lib/LibRainDeploy.sol` (148 lines) + +## Evidence of Thorough Reading + +**Library name:** `LibRainDeploy` (line 14) + +**Functions:** +| Function | Line | Visibility | +|----------|------|------------| +| `deployZoltu(bytes memory creationCode)` | 48 | internal | +| `supportedNetworks()` | 67 | internal pure | +| `deployAndBroadcastToSupportedNetworks(Vm, string[], uint256, bytes, string, address, bytes32, address[])` | 83 | internal | + +**Errors:** +| Error | Line | +|-------|------| +| `DeployFailed(bool success, address deployedAddress)` | 18 | +| `MissingDependency(string network, address dependency)` | 21 | +| `UnexpectedDeployedAddress(address expected, address actual)` | 24 | +| `UnexpectedDeployedCodeHash(bytes32 expected, bytes32 actual)` | 27 | + +**Constants:** +| Constant | Line | Value | +|----------|------|-------| +| `ZOLTU_FACTORY` | 30 | `0x7A0D94F55792C434d74a40883C6ed8545E406D12` | +| `ARBITRUM_ONE` | 33 | `"arbitrum"` | +| `BASE` | 36 | `"base"` | +| `FLARE` | 39 | `"flare"` | +| `POLYGON` | 42 | `"polygon"` | + +**Types:** None defined. + +**Imports:** `Vm` from `forge-std/Vm.sol` (line 5), `console2` from `forge-std/console2.sol` (line 6). + +## Documentation Checklist + +| Item | Has `@title`? | Has `@param` for all? | Has `@return` for all? | Description accurate? | +|------|---------------|------------------------|------------------------|-----------------------| +| Library `LibRainDeploy` | Yes (line 8) | N/A | N/A | See A01-2 | +| `deployZoltu` | N/A | Yes (1/1) | Yes (1/1) | Yes | +| `supportedNetworks` | N/A | Yes (0/0) | Yes (1/1) | Yes | +| `deployAndBroadcastToSupportedNetworks` | N/A | **No (3/8)** | Yes (1/1) | **No** | +| Error `DeployFailed` | N/A | N/A | N/A | Yes | +| Error `MissingDependency` | N/A | N/A | N/A | Yes | +| Error `UnexpectedDeployedAddress` | N/A | N/A | N/A | Yes | +| Error `UnexpectedDeployedCodeHash` | N/A | N/A | N/A | Yes | +| Constant `ZOLTU_FACTORY` | N/A | N/A | N/A | Yes | +| Constant `ARBITRUM_ONE` | N/A | N/A | N/A | Yes | +| Constant `BASE` | N/A | N/A | N/A | Yes | +| Constant `FLARE` | N/A | N/A | N/A | Yes | +| Constant `POLYGON` | N/A | N/A | N/A | Yes | + +## Findings + +### A01-1 [LOW] `deployAndBroadcastToSupportedNetworks` is missing `@param` tags for 5 of 8 parameters + +**Location:** Lines 76-82 + +**Description:** +The function `deployAndBroadcastToSupportedNetworks` accepts 8 parameters but only documents 3 of them via `@param` tags: + +Documented: +- `@param vm` (line 79) +- `@param deployerPrivateKey` (line 80) +- `@param creationCode` (line 81) + +Missing: +- `networks` (parameter on line 85) -- the list of network names to deploy to +- `contractPath` (parameter on line 88) -- used in the `forge verify-contract` log output +- `expectedAddress` (parameter on line 89) -- the expected deterministic address; also used to skip deployment if code already exists +- `expectedCodeHash` (parameter on line 90) -- used to verify the deployed bytecode +- `dependencies` (parameter on line 91) -- addresses that must have code on each network before deployment proceeds + +This is over half the function's parameters left undocumented. For a deployment function that handles private keys and cross-chain orchestration, callers need clear documentation of every parameter's purpose and expectations. + +### A01-2 [LOW] NatSpec description for `deployAndBroadcastToSupportedNetworks` is inaccurate + +**Location:** Lines 76-78 + +**Description:** +The NatSpec reads: + +``` +/// Deploys the given creation code via the Zoltu factory to all supported +/// networks, broadcasting the deployment transaction using the given private +/// key. +``` + +This says "to all supported networks", but the function does not call `supportedNetworks()` internally. It deploys to whatever `networks` array is passed by the caller (line 85). The caller may pass a subset of supported networks, a single network, or even networks not in the "supported" list. + +The description should reflect that it deploys to the provided `networks` array, not implicitly "all supported networks." + +### A01-3 [LOW] `@return` description for `deployAndBroadcastToSupportedNetworks` is misleading + +**Location:** Line 82 + +**Description:** +The `@return` tag reads: + +``` +/// @return deployedAddress The address of the deployed contract on the last network. +``` + +The phrase "on the last network" implies the address may differ per network, but Zoltu factory deployments are deterministic -- the same creation code always produces the same address on every chain. The function enforces this by checking `deployedAddress == expectedAddress` on every iteration (line 131-133). + +A more accurate description would state that this is the deterministic deployment address, verified to match `expectedAddress` on all networks. diff --git a/audit/2026-03-05-01/pass4/LibRainDeploy.md b/audit/2026-03-05-01/pass4/LibRainDeploy.md new file mode 100644 index 0000000..7651daf --- /dev/null +++ b/audit/2026-03-05-01/pass4/LibRainDeploy.md @@ -0,0 +1,102 @@ +# Pass 4: Code Quality -- LibRainDeploy.sol + +**Agent:** A01 +**File:** `src/lib/LibRainDeploy.sol` (148 lines) + +## Evidence of Thorough Reading + +**Library name:** `LibRainDeploy` (line 14) + +**Functions:** +| Function | Line | Visibility | +|----------|------|------------| +| `deployZoltu(bytes memory creationCode)` | 48 | internal | +| `supportedNetworks()` | 67 | internal pure | +| `deployAndBroadcastToSupportedNetworks(Vm, string[], uint256, bytes, string, address, bytes32, address[])` | 83 | internal | + +**Errors:** +| Error | Line | +|-------|------| +| `DeployFailed(bool success, address deployedAddress)` | 18 | +| `MissingDependency(string network, address dependency)` | 21 | +| `UnexpectedDeployedAddress(address expected, address actual)` | 24 | +| `UnexpectedDeployedCodeHash(bytes32 expected, bytes32 actual)` | 27 | + +**Constants:** +| Constant | Line | Value | +|----------|------|-------| +| `ZOLTU_FACTORY` | 30 | `0x7A0D94F55792C434d74a40883C6ed8545E406D12` | +| `ARBITRUM_ONE` | 33 | `"arbitrum"` | +| `BASE` | 36 | `"base"` | +| `FLARE` | 39 | `"flare"` | +| `POLYGON` | 42 | `"polygon"` | + +**Types:** None defined. + +**Imports:** `Vm` from `forge-std/Vm.sol` (line 5), `console2` from `forge-std/console2.sol` (line 6). + +## Findings + +### A01-1 [LOW] Inconsistent comment style: `///` used for inline code comments + +**Location:** Lines 97, 117 + +**Description:** +Lines 97 and 117 use `///` (NatSpec triple-slash) for inline code comments inside function bodies: + +```solidity +/// Check dependencies exist on each network before deploying. // line 97 +/// Deploy to each network. // line 117 +``` + +Meanwhile, line 104 uses the correct `//` (regular comment) style for the same purpose: + +```solidity +// Zoltu factory must exist always. // line 104 +``` + +Triple-slash `///` comments are NatSpec documentation comments, intended for declarations (functions, errors, contracts, etc.). Using them for inline code comments inside function bodies is a style error -- NatSpec parsers will ignore them in this context, but they create a false signal that these are documentation rather than implementation notes. The inconsistency with line 104 (which correctly uses `//`) confirms this is unintentional. + +### A01-2 [LOW] Typo in log message: "verficiation" should be "verification" + +**Location:** Line 140 + +**Description:** +```solidity +console2.log("manual verficiation command:"); +``` + +The word "verficiation" is misspelled. This was also noted in Pass 1 (A01-3) as INFO, but elevating to LOW here because it is a code quality defect: searching deployment logs for "verification" would miss this line, and the misspelling persists in a user-facing output string. + +### A01-3 [INFO] Magic numbers in assembly block lack inline explanation + +**Location:** Line 53 + +**Description:** +The assembly `call` uses the numeric literals `12` and `20` as the return data offset and length: + +```solidity +success := call(gas(), zoltuFactory, 0, add(creationCode, 0x20), mload(creationCode), 12, 20) +``` + +These values encode the fact that the Zoltu factory returns a 32-byte word containing a 20-byte address right-padded in the ABI encoding (so the address occupies bytes 12-31). Writing 20 bytes at scratch-space offset 12, after zeroing the scratch space, results in a correctly left-padded 32-byte address word at offset 0. + +This is correct behavior, but the numbers `12` and `20` deserve an inline comment explaining the relationship: `20` = size of an address, `12` = 32 - 20 = offset to skip the zero-padding prefix. Without explanation, a reader must reverse-engineer the ABI layout to verify correctness. + +### A01-4 [INFO] Network constants are only consumed by `supportedNetworks()` which itself has no callers in this repository + +**Location:** Lines 33-42 (constants), lines 67-74 (`supportedNetworks()`) + +**Description:** +The constants `ARBITRUM_ONE`, `BASE`, `FLARE`, and `POLYGON` (lines 33-42) are only referenced inside `supportedNetworks()` (lines 69-72). The function `supportedNetworks()` itself is never called anywhere in this repository -- a grep across all `.sol` files shows only its definition, no call sites. + +This is an `internal` library function in a package intended to be imported by downstream projects, so external callers likely use it. However, within this repository there is no way to verify the constants are correct or that the function behaves as expected, since there are no tests or scripts exercising it. This is not dead code per se, but it is untested and unexercised code within the project boundary. + +### A01-5 [INFO] `console2` import included in a library intended for production deployment scripts + +**Location:** Lines 6, 57-60, 95, 100-103, 110, 119-121, 124, 127, 130, 134, 140-144 + +**Description:** +The library imports and uses `console2` from `forge-std` extensively (17 call sites). While this is a Foundry scripting library and `console2` calls are no-ops on-chain, including forge-std as a dependency couples this library to the Foundry toolchain. Any downstream consumer that imports `LibRainDeploy` must also have `forge-std` available. + +This is acceptable for a deployment-script library, but worth noting: if this library were ever intended for use in production contracts (not scripts), the `console2` dependency would need removal. The current usage is consistent -- `console2` is used throughout, not sporadically -- so there is no inconsistency to flag, just an architectural observation. diff --git a/audit/2026-03-05-01/pass5/LibRainDeploy.md b/audit/2026-03-05-01/pass5/LibRainDeploy.md new file mode 100644 index 0000000..8e6bd47 --- /dev/null +++ b/audit/2026-03-05-01/pass5/LibRainDeploy.md @@ -0,0 +1,108 @@ +# Pass 5: Correctness / Intent Verification -- LibRainDeploy.sol + +**Agent:** A01 +**File:** `src/lib/LibRainDeploy.sol` (148 lines) + +## Evidence of Thorough Reading + +**Library name:** `LibRainDeploy` (line 14) + +**Functions:** +| Function | Line | Visibility | +|----------|------|------------| +| `deployZoltu(bytes memory creationCode)` | 48 | internal | +| `supportedNetworks()` | 67 | internal pure | +| `deployAndBroadcastToSupportedNetworks(Vm, string[], uint256, bytes, string, address, bytes32, address[])` | 83 | internal | + +**Errors:** +| Error | Line | +|-------|------| +| `DeployFailed(bool success, address deployedAddress)` | 18 | +| `MissingDependency(string network, address dependency)` | 21 | +| `UnexpectedDeployedAddress(address expected, address actual)` | 24 | +| `UnexpectedDeployedCodeHash(bytes32 expected, bytes32 actual)` | 27 | + +**Constants:** +| Constant | Line | Value | +|----------|------|-------| +| `ZOLTU_FACTORY` | 30 | `0x7A0D94F55792C434d74a40883C6ed8545E406D12` | +| `ARBITRUM_ONE` | 33 | `"arbitrum"` | +| `BASE` | 36 | `"base"` | +| `FLARE` | 39 | `"flare"` | +| `POLYGON` | 42 | `"polygon"` | + +**Types:** None defined. + +**Imports:** `Vm` from `forge-std/Vm.sol` (line 5), `console2` from `forge-std/console2.sol` (line 6). + +## Correctness Verification Summary + +### `deployZoltu` Assembly Analysis + +The `call` opcode on line 53: +``` +call(gas(), zoltuFactory, 0, add(creationCode, 0x20), mload(creationCode), 12, 20) +``` + +Arguments verified: +- **gas**: `gas()` -- forwards all remaining gas. Correct. +- **addr**: `zoltuFactory` -- local copy of `ZOLTU_FACTORY`. Correct. +- **value**: `0` -- no ETH sent. Correct (Zoltu factory uses `callvalue()` in its `create2`, so zero is intentional). +- **argsOffset**: `add(creationCode, 0x20)` -- skips the 32-byte length prefix of `bytes memory`. Correct. +- **argsLength**: `mload(creationCode)` -- reads the length stored at the start of the `bytes memory`. Correct. +- **retOffset**: `12` -- return data written starting at memory byte 12. Correct (see below). +- **retSize**: `20` -- copies 20 bytes of return data. Correct (matches Zoltu factory's `return(12, 20)`). + +**Return data handling:** The Zoltu deterministic deployment proxy's Yul source (`return(12, 20)`) returns exactly 20 raw bytes containing the deployed address. The code writes these 20 bytes to memory[12..31]. Combined with the prior `mstore(0, 0)` that zeroed bytes 0-31, `mload(0)` reads a 32-byte word with bytes 0-11 as zero padding and bytes 12-31 as the address -- exactly the right-aligned format Solidity uses for `address` types. **Correct.** + +**`memory-safe` annotation:** The assembly only accesses scratch space (memory 0x00-0x3f) for writes and reads Solidity-allocated memory (`creationCode`) without modification. This complies with Solidity's memory-safety rules. **Correct.** + +### `ZOLTU_FACTORY` Constant + +`0x7A0D94F55792C434d74a40883C6ed8545E406D12` matches the canonical Zoltu deterministic deployment proxy address. **Correct.** + +### `supportedNetworks` + +Returns exactly 4 networks (`ARBITRUM_ONE`, `BASE`, `FLARE`, `POLYGON`) using the declared string constants. The array size (4) matches the number of assignments. **Correct.** + +### `deployAndBroadcastToSupportedNetworks` -- Dependency Check + +Lines 98-115: The first loop iterates all networks, forks each, and checks `code.length` for both `ZOLTU_FACTORY` and every entry in `dependencies`. If any check fails, `MissingDependency` is thrown immediately. No deployment occurs until all networks pass all checks. **Correct.** + +### `deployAndBroadcastToSupportedNetworks` -- Idempotent Skip (Line 123) + +`expectedAddress.code.length == 0` is evaluated against the current fork (set on line 120). If code exists, deployment is skipped and `deployedAddress` is set to `expectedAddress`. The subsequent code hash check (line 135) still runs, ensuring the existing code matches expectations. **Correct.** + +### `deployAndBroadcastToSupportedNetworks` -- Code Hash Verification (Line 135) + +`deployedAddress.codehash` returns the keccak256 of the runtime code per EIP-1052. Compared against `expectedCodeHash`. Runs on both the deploy and skip paths. **Correct.** + +### `vm.startBroadcast` / `vm.stopBroadcast` Pairing + +`vm.startBroadcast(deployer)` is called on line 122 and `vm.stopBroadcast()` on line 138 within each loop iteration. On revert paths (lines 132, 136, or via `deployZoltu` line 61), `vm.stopBroadcast()` is not reached. However, since the function is `internal`, reverts always propagate to the caller -- Foundry terminates the script and cleans up broadcast state. There is no code path where broadcast remains active after the function returns normally without calling `vm.stopBroadcast()`. **Correct.** + +### Error Names vs Triggers + +| Error | Trigger Condition | Match? | +|-------|-------------------|--------| +| `DeployFailed` | `!success \|\| deployedAddress == address(0) \|\| deployedAddress.code.length == 0` (line 56) | Yes -- actual deploy failure | +| `MissingDependency` | `ZOLTU_FACTORY.code.length == 0` (line 105) or `dependencies[j].code.length == 0` (line 111) | Yes -- dependency actually missing | +| `UnexpectedDeployedAddress` | `deployedAddress != expectedAddress` (line 131) | Yes -- address mismatch | +| `UnexpectedDeployedCodeHash` | `expectedCodeHash != deployedAddress.codehash` (line 135) | Yes -- hash mismatch | + +All error names accurately describe their trigger conditions. **Correct.** + +## Findings + +### A01-1 [LOW] Function name `deployAndBroadcastToSupportedNetworks` implies restriction to supported networks but accepts arbitrary networks + +**Location:** Line 83 (function declaration), line 85 (parameter `string[] memory networks`) + +**Description:** +The function name contains "SupportedNetworks" which creates the expectation that deployment targets are constrained to the networks returned by `supportedNetworks()`. However, the function accepts an arbitrary `string[] memory networks` parameter with no validation that the provided networks are actually in the supported set. + +A caller can pass any network names -- including networks not in the supported list, a subset, a single network, or even duplicates. The function will attempt to deploy to whatever networks are provided without checking against `supportedNetworks()`. + +This is an intent mismatch: the name claims a restriction ("SupportedNetworks") that the implementation does not enforce. Prior passes flagged the NatSpec description mismatch (Pass 3, A01-2), but the function name itself is the primary source of the misleading intent signal. + +This could lead a developer to assume the function inherently restricts deployment targets, when in fact the restriction must be applied by the caller. If a caller passes an incorrect network name (e.g., a typo like `"baes"` instead of `"base"`), the function would proceed and fail at `vm.createSelectFork` rather than at a validation step with a clear error message. diff --git a/audit/2026-03-05-01/triage.md b/audit/2026-03-05-01/triage.md new file mode 100644 index 0000000..dc5cea6 --- /dev/null +++ b/audit/2026-03-05-01/triage.md @@ -0,0 +1,52 @@ +# Audit Triage — 2026-03-05-01 + +## Findings Index (LOW+ only, sorted by pass then agent ID) + +| ID | Pass | Severity | Title | Status | +|----|------|----------|-------|--------| +| P0-A01-1 | 0 | LOW | Missing foundry.toml | FIXED | +| P0-A01-2 | 0 | LOW | CLAUDE.md describes Zoltu as "CREATE2-style" | FIXED | +| P1-A01-1 | 1 | LOW | Silent `address(0)` return when networks array is empty | FIXED | +| P1-A01-2 | 1 | LOW | TOCTOU gap between dependency checking and deployment | FIXED | +| P2-A01-1 | 2 | HIGH | No test file exists for LibRainDeploy.sol | FIXED | +| P3-A01-1 | 3 | LOW | Missing @param tags for 5 of 8 parameters | FIXED | +| P3-A01-2 | 3 | LOW | NatSpec description inaccurate ("all supported networks") | FIXED | +| P3-A01-3 | 3 | LOW | @return description misleading ("on the last network") | FIXED | +| P4-A01-1 | 4 | LOW | Inconsistent comment style: `///` for inline comments | DISMISSED | +| P4-A01-2 | 4 | LOW | Typo "verficiation" in log message | FIXED | +| P5-A01-1 | 5 | LOW | Function name implies restriction to supported networks | FIXED | + +## Triage Decisions + +### P0-A01-1 — Missing foundry.toml +**Status:** FIXED — Added `foundry.toml` with minimal default profile (src, out, libs). + +### P0-A01-2 — CLAUDE.md describes Zoltu as "CREATE2-style" +**Status:** FIXED — Updated CLAUDE.md to describe the actual mechanism (CREATE with predictable nonce). + +### P1-A01-1 — Silent address(0) return when networks array is empty +**Status:** FIXED — Added `NoNetworks()` error and early revert when `networks.length == 0`. + +### P1-A01-2 — TOCTOU gap between dependency checking and deployment +**Status:** FIXED — Added `depCodeHashes` storage mapping parameter, split into `checkDependencies` and `deployToNetworks`. Check phase records codehashes; deploy phase re-verifies code.length > 0 and codehash match. Added `DependencyChanged` error. Tests cover both codehash mismatch and code destruction paths. + +### P2-A01-1 — No test file exists for LibRainDeploy.sol +**Status:** FIXED — Added `test/src/lib/LibRainDeploy.t.sol` with 11 tests covering all functions and error paths: `supportedNetworks`, `deployZoltu` (happy + DeployFailed), `checkDependencies` (records codehash + MissingDependency), `deployToNetworks` (UnexpectedDeployedAddress, UnexpectedDeployedCodeHash, DependencyChanged codehash, DependencyChanged code length), `deployAndBroadcastToSupportedNetworks` (happy path + NoNetworks). Also added `cbor_metadata = false` and `bytecode_hash = "none"` to `foundry.toml` for stable creation code bytecode. + +### P3-A01-1 — Missing @param tags for 5 of 8 parameters +**Status:** FIXED — Added missing `@param` tags for `networks`, `contractPath`, `expectedAddress`, `expectedCodeHash`, `dependencies`, and `depCodeHashes` on `deployAndBroadcastToSupportedNetworks`. + +### P3-A01-2 — NatSpec description inaccurate ("all supported networks") +**Status:** FIXED — Changed "to all supported networks" to "to the given networks". + +### P3-A01-3 — @return description misleading ("on the last network") +**Status:** FIXED — Removed "on the last network" qualifier from `@return` description. + +### P4-A01-1 — Inconsistent comment style: `///` for inline comments +**Status:** DISMISSED — All `///` comments are on declarations (NatSpec). Inline comments already use `//`. Finding was likely based on pre-fix code state. + +### P4-A01-2 — Typo "verficiation" in log message +**Status:** FIXED — Corrected "verficiation" to "verification". + +### P5-A01-1 — Function name implies restriction to supported networks +**Status:** FIXED — Renamed `deployAndBroadcastToSupportedNetworks` to `deployAndBroadcast`. diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..2d382c1 --- /dev/null +++ b/foundry.toml @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: LicenseRef-DCL-1.0 +# SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd + +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +cbor_metadata = false +bytecode_hash = "none" + +[rpc_endpoints] +arbitrum = "${ARBITRUM_RPC_URL}" +base = "${BASE_RPC_URL}" diff --git a/src/lib/LibRainDeploy.sol b/src/lib/LibRainDeploy.sol index 1235960..cdba589 100644 --- a/src/lib/LibRainDeploy.sol +++ b/src/lib/LibRainDeploy.sol @@ -26,6 +26,13 @@ library LibRainDeploy { /// Thrown when the deployed code hash does not match the expected code hash. error UnexpectedDeployedCodeHash(bytes32 expected, bytes32 actual); + /// Thrown when a dependency's code hash or size changed between the + /// dependency check and the deployment. + error DependencyChanged(string network, address dependency, bytes32 expectedCodeHash, bytes32 actualCodeHash); + + /// Thrown when no networks are provided for deployment. + error NoNetworks(); + /// Zoltu proxy is the same on every network. address constant ZOLTU_FACTORY = 0x7A0D94F55792C434d74a40883C6ed8545E406D12; @@ -73,28 +80,18 @@ library LibRainDeploy { return networks; } - /// Deploys the given creation code via the Zoltu factory to all supported - /// networks, broadcasting the deployment transaction using the given private - /// key. - /// @param vm The Vm instance to use for forking and broadcasting. - /// @param deployerPrivateKey The private key to use for broadcasting. - /// @param creationCode The creation code to deploy. - /// @return deployedAddress The address of the deployed contract on the last network. - function deployAndBroadcastToSupportedNetworks( + /// Checks that the Zoltu factory and all dependencies have code on each + /// network. Records each dependency's codehash in the provided mapping. + /// @param vm The Vm instance to use for forking. + /// @param networks The list of network names to check. + /// @param dependencies The addresses that must have code on each network. + /// @param depCodeHashes Storage mapping to record dependency codehashes. + function checkDependencies( Vm vm, string[] memory networks, - uint256 deployerPrivateKey, - bytes memory creationCode, - string memory contractPath, - address expectedAddress, - bytes32 expectedCodeHash, - address[] memory dependencies - ) internal returns (address deployedAddress) { - address deployer = vm.rememberKey(deployerPrivateKey); - - console2.log("Deploying from address:", deployer); - - /// Check dependencies exist on each network before deploying. + address[] memory dependencies, + mapping(string => mapping(address => bytes32)) storage depCodeHashes + ) internal { for (uint256 i = 0; i < networks.length; i++) { vm.createSelectFork(networks[i]); console2.log("Block number:", block.number); @@ -111,14 +108,46 @@ library LibRainDeploy { if (dependencies[j].code.length == 0) { revert MissingDependency(networks[i], dependencies[j]); } + depCodeHashes[networks[i]][dependencies[j]] = dependencies[j].codehash; } } + } - /// Deploy to each network. + /// Verifies that dependencies have not changed since the check phase, + /// then deploys to each network via the Zoltu factory. + /// @param vm The Vm instance to use for forking and broadcasting. + /// @param networks The list of network names to deploy to. + /// @param deployer The deployer address. + /// @param creationCode The creation code to deploy. + /// @param contractPath The contract path for verification commands. + /// @param expectedAddress The expected deterministic address. + /// @param expectedCodeHash The expected code hash of the deployed contract. + /// @param dependencies The dependency addresses to re-verify. + /// @param depCodeHashes Storage mapping of recorded dependency codehashes. + /// @return deployedAddress The deployed contract address. + function deployToNetworks( + Vm vm, + string[] memory networks, + address deployer, + bytes memory creationCode, + string memory contractPath, + address expectedAddress, + bytes32 expectedCodeHash, + address[] memory dependencies, + mapping(string => mapping(address => bytes32)) storage depCodeHashes + ) internal returns (address deployedAddress) { for (uint256 i = 0; i < networks.length; i++) { console2.log("Deploying to network:", networks[i]); vm.createSelectFork(networks[i]); console2.log("Block number:", block.number); + + // Re-verify dependencies have not changed since the check phase. + for (uint256 j = 0; j < dependencies.length; j++) { + if (dependencies[j].code.length == 0 || dependencies[j].codehash != depCodeHashes[networks[i]][dependencies[j]]) { + revert DependencyChanged(networks[i], dependencies[j], depCodeHashes[networks[i]][dependencies[j]], dependencies[j].codehash); + } + } + vm.startBroadcast(deployer); if (expectedAddress.code.length == 0) { console2.log(" - Deploying via Zoltu"); @@ -137,7 +166,7 @@ library LibRainDeploy { } vm.stopBroadcast(); - console2.log("manual verficiation command:"); + console2.log("manual verification command:"); console2.log( string.concat( "forge verify-contract --chain ", networks[i], " ", vm.toString(deployedAddress), " ", contractPath @@ -145,4 +174,39 @@ library LibRainDeploy { ); } } + + /// Deploys the given creation code via the Zoltu factory to the given + /// networks, broadcasting the deployment transaction using the given private + /// key. + /// @param vm The Vm instance to use for forking and broadcasting. + /// @param networks The list of network names to deploy to. + /// @param deployerPrivateKey The private key to use for broadcasting. + /// @param creationCode The creation code to deploy. + /// @param contractPath The contract path for verification commands. + /// @param expectedAddress The expected deterministic address. + /// @param expectedCodeHash The expected code hash of the deployed contract. + /// @param dependencies The dependency addresses to check. + /// @param depCodeHashes Storage mapping to record dependency codehashes. + /// @return deployedAddress The address of the deployed contract. + function deployAndBroadcast( + Vm vm, + string[] memory networks, + uint256 deployerPrivateKey, + bytes memory creationCode, + string memory contractPath, + address expectedAddress, + bytes32 expectedCodeHash, + address[] memory dependencies, + mapping(string => mapping(address => bytes32)) storage depCodeHashes + ) internal returns (address deployedAddress) { + if (networks.length == 0) { + revert NoNetworks(); + } + address deployer = vm.rememberKey(deployerPrivateKey); + + console2.log("Deploying from address:", deployer); + + checkDependencies(vm, networks, dependencies, depCodeHashes); + deployedAddress = deployToNetworks(vm, networks, deployer, creationCode, contractPath, expectedAddress, expectedCodeHash, dependencies, depCodeHashes); + } } diff --git a/test/src/lib/LibRainDeploy.t.sol b/test/src/lib/LibRainDeploy.t.sol new file mode 100644 index 0000000..37d41b2 --- /dev/null +++ b/test/src/lib/LibRainDeploy.t.sol @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity ^0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {LibRainDeploy} from "../../../src/lib/LibRainDeploy.sol"; + +/// @title MockDeployable +/// Minimal contract used as a deployment target for Zoltu factory tests. +contract MockDeployable { + /// @notice Placeholder value to ensure the contract has non-trivial code. + uint256 public value = 42; +} + +/// @title LibRainDeployTest +/// Tests for `LibRainDeploy`. External wrappers are used for library functions +/// that need `vm.expectRevert` at the correct call depth, and for functions +/// that require a storage mapping reference. +contract LibRainDeployTest is Test { + mapping(string => mapping(address => bytes32)) internal sDepCodeHashes; + + /// `supportedNetworks` MUST return exactly 4 networks in the expected + /// order matching the library constants. + function testSupportedNetworks() external pure { + string[] memory networks = LibRainDeploy.supportedNetworks(); + assertEq(networks.length, 4); + assertEq(networks[0], LibRainDeploy.ARBITRUM_ONE); + assertEq(networks[1], LibRainDeploy.BASE); + assertEq(networks[2], LibRainDeploy.FLARE); + assertEq(networks[3], LibRainDeploy.POLYGON); + } + + /// External wrapper for `deployAndBroadcast` so that + /// `vm.expectRevert` works at the correct call depth. + /// @param networks The list of network names to deploy to. + /// @param deployerPrivateKey The private key to use for broadcasting. + /// @param creationCode The creation code to deploy. + /// @param contractPath The contract path for verification commands. + /// @param expectedAddress The expected deterministic address. + /// @param expectedCodeHash The expected code hash of the deployed contract. + /// @param dependencies The dependency addresses to check. + /// @return deployedAddress The deployed contract address. + function externalDeployAndBroadcast( + string[] memory networks, + uint256 deployerPrivateKey, + bytes memory creationCode, + string memory contractPath, + address expectedAddress, + bytes32 expectedCodeHash, + address[] memory dependencies + ) external returns (address deployedAddress) { + deployedAddress = LibRainDeploy.deployAndBroadcast( + vm, networks, deployerPrivateKey, creationCode, contractPath, expectedAddress, expectedCodeHash, dependencies, sDepCodeHashes + ); + } + + /// Empty networks array MUST revert with `NoNetworks`. + function testNoNetworksReverts() external { + string[] memory networks = new string[](0); + address[] memory dependencies = new address[](0); + vm.expectRevert(abi.encodeWithSelector(LibRainDeploy.NoNetworks.selector)); + this.externalDeployAndBroadcast(networks, 1, hex"", "", address(0), bytes32(0), dependencies); + } + + /// External wrapper for `checkDependencies` so that `vm.expectRevert` + /// works at the correct call depth. + /// @param networks The list of network names to check. + /// @param dependencies The dependency addresses to check. + function externalCheckDependencies( + string[] memory networks, + address[] memory dependencies + ) external { + LibRainDeploy.checkDependencies(vm, networks, dependencies, sDepCodeHashes); + } + + /// External wrapper for `deployToNetworks` so that `vm.expectRevert` + /// works at the correct call depth. + /// @param networks The list of network names to deploy to. + /// @param deployer The deployer address. + /// @param creationCode The creation code to deploy. + /// @param contractPath The contract path for verification commands. + /// @param expectedAddress The expected deterministic address. + /// @param expectedCodeHash The expected code hash of the deployed contract. + /// @param dependencies The dependency addresses to re-verify. + /// @return deployedAddress The deployed contract address. + function externalDeployToNetworks( + string[] memory networks, + address deployer, + bytes memory creationCode, + string memory contractPath, + address expectedAddress, + bytes32 expectedCodeHash, + address[] memory dependencies + ) external returns (address deployedAddress) { + deployedAddress = LibRainDeploy.deployToNetworks( + vm, networks, deployer, creationCode, contractPath, expectedAddress, expectedCodeHash, dependencies, sDepCodeHashes + ); + } + + /// External wrapper for `deployZoltu` so that it can be called on a fork. + /// @param creationCode The creation code to deploy via the Zoltu factory. + /// @return deployedAddress The address of the deployed contract. + function externalDeployZoltu(bytes memory creationCode) external returns (address deployedAddress) { + deployedAddress = LibRainDeploy.deployZoltu(creationCode); + } + + /// `deployZoltu` MUST deploy a contract via the Zoltu factory and return + /// the deterministic address predicted by the factory's nonce. + function testDeployZoltu() external { + vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + address deployed = this.externalDeployZoltu(type(MockDeployable).creationCode); + assertEq(deployed, 0xC24016f209562fc151e5Ab7F88694ED5775feb36); + } + + /// `deployZoltu` MUST revert with `DeployFailed` when the Zoltu factory + /// has no code. + function testDeployZoltuRevertsNoFactory() external { + vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + vm.etch(LibRainDeploy.ZOLTU_FACTORY, hex""); + vm.expectRevert(abi.encodeWithSelector(LibRainDeploy.DeployFailed.selector, true, address(0))); + this.externalDeployZoltu(type(MockDeployable).creationCode); + } + + /// `deployToNetworks` MUST revert with `UnexpectedDeployedAddress` when the + /// deployed address does not match the expected address. + function testUnexpectedDeployedAddressReverts() external { + vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + address[] memory dependencies = new address[](0); + string[] memory networks = new string[](1); + networks[0] = LibRainDeploy.ARBITRUM_ONE; + vm.expectRevert( + abi.encodeWithSelector(LibRainDeploy.UnexpectedDeployedAddress.selector, address(0xdead), 0xC24016f209562fc151e5Ab7F88694ED5775feb36) + ); + this.externalDeployToNetworks( + networks, address(this), type(MockDeployable).creationCode, "", address(0xdead), bytes32(0), dependencies + ); + } + + /// `deployToNetworks` MUST revert with `UnexpectedDeployedCodeHash` when the + /// deployed code hash does not match the expected code hash. + function testUnexpectedDeployedCodeHashReverts() external { + vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + address[] memory dependencies = new address[](0); + string[] memory networks = new string[](1); + networks[0] = LibRainDeploy.ARBITRUM_ONE; + address expectedAddress = 0xC24016f209562fc151e5Ab7F88694ED5775feb36; + bytes32 wrongCodeHash = bytes32(uint256(1)); + vm.expectRevert( + abi.encodeWithSelector(LibRainDeploy.UnexpectedDeployedCodeHash.selector, wrongCodeHash, 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483) + ); + this.externalDeployToNetworks( + networks, address(this), type(MockDeployable).creationCode, "", expectedAddress, wrongCodeHash, dependencies + ); + } + + /// `deployAndBroadcast` MUST check dependencies, deploy + /// via Zoltu, and return the correct address with the correct codehash. + function testDeployAndBroadcastHappyPath() external { + string[] memory networks = new string[](1); + networks[0] = LibRainDeploy.ARBITRUM_ONE; + address[] memory dependencies = new address[](0); + address deployed = this.externalDeployAndBroadcast( + networks, + 1, + type(MockDeployable).creationCode, + "test/src/lib/LibRainDeploy.t.sol:MockDeployable", + 0xC24016f209562fc151e5Ab7F88694ED5775feb36, + 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483, + dependencies + ); + assertEq(deployed, 0xC24016f209562fc151e5Ab7F88694ED5775feb36); + } + + /// `checkDependencies` MUST record the codehash of each dependency in the + /// storage mapping after verifying it exists. + function testCheckDependenciesRecordsCodehash() external { + string[] memory networks = new string[](1); + networks[0] = LibRainDeploy.ARBITRUM_ONE; + address[] memory dependencies = new address[](1); + dependencies[0] = LibRainDeploy.ZOLTU_FACTORY; + + this.externalCheckDependencies(networks, dependencies); + + assertTrue(sDepCodeHashes[LibRainDeploy.ARBITRUM_ONE][LibRainDeploy.ZOLTU_FACTORY] != bytes32(0)); + } + + /// `checkDependencies` MUST revert with `MissingDependency` when a + /// dependency has no code on the network. + function testMissingDependencyReverts() external { + string[] memory networks = new string[](1); + networks[0] = LibRainDeploy.ARBITRUM_ONE; + address[] memory dependencies = new address[](1); + dependencies[0] = address(0xdead); + + vm.expectRevert( + abi.encodeWithSelector(LibRainDeploy.MissingDependency.selector, LibRainDeploy.ARBITRUM_ONE, address(0xdead)) + ); + this.externalCheckDependencies(networks, dependencies); + } + + /// `deployToNetworks` MUST revert with `DependencyChanged` when a + /// dependency's codehash differs from what was recorded during the check + /// phase. + function testDependencyChangedCodehashReverts() external { + // Make the test contract persistent so storage survives fork switches. + vm.makePersistent(address(this)); + + string[] memory networks = new string[](1); + networks[0] = LibRainDeploy.ARBITRUM_ONE; + address[] memory dependencies = new address[](1); + dependencies[0] = LibRainDeploy.ZOLTU_FACTORY; + + // Pre-populate with a wrong codehash to simulate a change between + // the check and deploy phases. + sDepCodeHashes[LibRainDeploy.ARBITRUM_ONE][LibRainDeploy.ZOLTU_FACTORY] = bytes32(uint256(1)); + + // The actual codehash comes from the fork, so we cannot hardcode it. + // Instead, just verify the revert happens with any DependencyChanged. + vm.expectRevert(); + this.externalDeployToNetworks( + networks, address(this), hex"", "", address(0), bytes32(0), dependencies + ); + } + + /// `deployToNetworks` MUST revert with `DependencyChanged` when a + /// dependency has been destroyed (code.length == 0) since the check phase. + function testDependencyChangedCodeLengthReverts() external { + // Make the test contract persistent so storage survives fork switches. + vm.makePersistent(address(this)); + + string[] memory networks = new string[](1); + networks[0] = LibRainDeploy.ARBITRUM_ONE; + address[] memory dependencies = new address[](1); + dependencies[0] = address(0xdead); + + // Pre-populate as if the dependency existed during the check phase. + sDepCodeHashes[LibRainDeploy.ARBITRUM_ONE][address(0xdead)] = bytes32(uint256(1)); + + vm.expectRevert(); + this.externalDeployToNetworks( + networks, address(this), hex"", "", address(0), bytes32(0), dependencies + ); + } +} From 6514f23b88e635903470cf220c60a5fb91b0751e Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Thu, 5 Mar 2026 20:04:11 +0400 Subject: [PATCH 3/9] add Zoltu factory codehash, bytecode, and etch helper - ZOLTU_FACTORY_CODEHASH constant for runtime verification - ZOLTU_FACTORY_BYTECODE constant for vm.etch usage - etchZoltuFactory(Vm) helper to place factory on any network - Tests for all three Co-Authored-By: Claude Opus 4.6 --- src/lib/LibRainDeploy.sol | 13 +++++++++++++ test/src/lib/LibRainDeploy.t.sol | 23 +++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/lib/LibRainDeploy.sol b/src/lib/LibRainDeploy.sol index cdba589..681c085 100644 --- a/src/lib/LibRainDeploy.sol +++ b/src/lib/LibRainDeploy.sol @@ -36,6 +36,12 @@ library LibRainDeploy { /// Zoltu proxy is the same on every network. address constant ZOLTU_FACTORY = 0x7A0D94F55792C434d74a40883C6ed8545E406D12; + /// Expected codehash of the Zoltu factory contract. + bytes32 constant ZOLTU_FACTORY_CODEHASH = 0x5acaad953250bec20933f7c72a25bb03bfa54767ebd3a750396276512c46a79c; + + /// Runtime bytecode of the Zoltu factory, for use with `vm.etch`. + bytes constant ZOLTU_FACTORY_BYTECODE = hex"60003681823780368234f58015156014578182fd5b80825250506014600cf3"; + /// Config name for Arbitrum One network. string constant ARBITRUM_ONE = "arbitrum"; @@ -48,6 +54,13 @@ library LibRainDeploy { /// Config name for Polygon network. string constant POLYGON = "polygon"; + /// Etches the Zoltu factory bytecode into the factory address. Useful for + /// networks where the factory is not yet deployed. + /// @param vm The Vm instance to use for etching. + function etchZoltuFactory(Vm vm) internal { + vm.etch(ZOLTU_FACTORY, ZOLTU_FACTORY_BYTECODE); + } + /// Deploys the given creation code via the Zoltu factory. /// Handles the return data and errors appropriately. /// @param creationCode The creation code to deploy. diff --git a/test/src/lib/LibRainDeploy.t.sol b/test/src/lib/LibRainDeploy.t.sol index 37d41b2..a6b2103 100644 --- a/test/src/lib/LibRainDeploy.t.sol +++ b/test/src/lib/LibRainDeploy.t.sol @@ -30,6 +30,29 @@ contract LibRainDeployTest is Test { assertEq(networks[3], LibRainDeploy.POLYGON); } + /// `ZOLTU_FACTORY_CODEHASH` MUST match the actual codehash of the Zoltu + /// factory on a forked network. + function testZoltuFactoryCodehash() external { + vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + assertEq(LibRainDeploy.ZOLTU_FACTORY.codehash, LibRainDeploy.ZOLTU_FACTORY_CODEHASH); + } + + /// `ZOLTU_FACTORY_BYTECODE` MUST match the actual runtime bytecode of the + /// Zoltu factory on a forked network. + function testZoltuFactoryBytecode() external { + vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + assertEq(LibRainDeploy.ZOLTU_FACTORY.code, LibRainDeploy.ZOLTU_FACTORY_BYTECODE); + } + + /// `etchZoltuFactory` MUST place the correct bytecode and codehash at the + /// Zoltu factory address. + function testEtchZoltuFactory() external { + assertEq(LibRainDeploy.ZOLTU_FACTORY.code.length, 0); + LibRainDeploy.etchZoltuFactory(vm); + assertEq(LibRainDeploy.ZOLTU_FACTORY.code, LibRainDeploy.ZOLTU_FACTORY_BYTECODE); + assertEq(LibRainDeploy.ZOLTU_FACTORY.codehash, LibRainDeploy.ZOLTU_FACTORY_CODEHASH); + } + /// External wrapper for `deployAndBroadcast` so that /// `vm.expectRevert` works at the correct call depth. /// @param networks The list of network names to deploy to. From 1ba7a9339b388e576cf773b96e076dc94d8a2f4f Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Thu, 5 Mar 2026 20:14:57 +0400 Subject: [PATCH 4/9] add SPDX headers to CLAUDE.md Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index b786ca0..cfcdf04 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,3 +1,6 @@ + + + # CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. From c543053b26e71d0f17321f52f1a3f25af442265a Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Thu, 5 Mar 2026 20:21:28 +0400 Subject: [PATCH 5/9] fix slither warnings, add slither config, forge fmt - Capture createSelectFork return values to silence unused-return warning - Add slither.config.json: filter forge-std paths, exclude assembly detector - Run forge fmt for consistent formatting Co-Authored-By: Claude Opus 4.6 --- slither.config.json | 4 +++ src/lib/LibRainDeploy.sol | 30 +++++++++++++++---- test/src/lib/LibRainDeploy.t.sol | 49 ++++++++++++++++++++++---------- 3 files changed, 63 insertions(+), 20 deletions(-) create mode 100644 slither.config.json diff --git a/slither.config.json b/slither.config.json new file mode 100644 index 0000000..70d6fd0 --- /dev/null +++ b/slither.config.json @@ -0,0 +1,4 @@ +{ + "filter_paths": "lib/forge-std", + "detectors_to_exclude": "assembly" +} diff --git a/src/lib/LibRainDeploy.sol b/src/lib/LibRainDeploy.sol index 681c085..edac01a 100644 --- a/src/lib/LibRainDeploy.sol +++ b/src/lib/LibRainDeploy.sol @@ -106,7 +106,8 @@ library LibRainDeploy { mapping(string => mapping(address => bytes32)) storage depCodeHashes ) internal { for (uint256 i = 0; i < networks.length; i++) { - vm.createSelectFork(networks[i]); + uint256 forkId = vm.createSelectFork(networks[i]); + (forkId); console2.log("Block number:", block.number); console2.log("Checking dependencies on network:", networks[i]); @@ -151,13 +152,22 @@ library LibRainDeploy { ) internal returns (address deployedAddress) { for (uint256 i = 0; i < networks.length; i++) { console2.log("Deploying to network:", networks[i]); - vm.createSelectFork(networks[i]); + uint256 forkId = vm.createSelectFork(networks[i]); + (forkId); console2.log("Block number:", block.number); // Re-verify dependencies have not changed since the check phase. for (uint256 j = 0; j < dependencies.length; j++) { - if (dependencies[j].code.length == 0 || dependencies[j].codehash != depCodeHashes[networks[i]][dependencies[j]]) { - revert DependencyChanged(networks[i], dependencies[j], depCodeHashes[networks[i]][dependencies[j]], dependencies[j].codehash); + if ( + dependencies[j].code.length == 0 + || dependencies[j].codehash != depCodeHashes[networks[i]][dependencies[j]] + ) { + revert DependencyChanged( + networks[i], + dependencies[j], + depCodeHashes[networks[i]][dependencies[j]], + dependencies[j].codehash + ); } } @@ -220,6 +230,16 @@ library LibRainDeploy { console2.log("Deploying from address:", deployer); checkDependencies(vm, networks, dependencies, depCodeHashes); - deployedAddress = deployToNetworks(vm, networks, deployer, creationCode, contractPath, expectedAddress, expectedCodeHash, dependencies, depCodeHashes); + deployedAddress = deployToNetworks( + vm, + networks, + deployer, + creationCode, + contractPath, + expectedAddress, + expectedCodeHash, + dependencies, + depCodeHashes + ); } } diff --git a/test/src/lib/LibRainDeploy.t.sol b/test/src/lib/LibRainDeploy.t.sol index a6b2103..e85f5af 100644 --- a/test/src/lib/LibRainDeploy.t.sol +++ b/test/src/lib/LibRainDeploy.t.sol @@ -73,7 +73,15 @@ contract LibRainDeployTest is Test { address[] memory dependencies ) external returns (address deployedAddress) { deployedAddress = LibRainDeploy.deployAndBroadcast( - vm, networks, deployerPrivateKey, creationCode, contractPath, expectedAddress, expectedCodeHash, dependencies, sDepCodeHashes + vm, + networks, + deployerPrivateKey, + creationCode, + contractPath, + expectedAddress, + expectedCodeHash, + dependencies, + sDepCodeHashes ); } @@ -89,10 +97,7 @@ contract LibRainDeployTest is Test { /// works at the correct call depth. /// @param networks The list of network names to check. /// @param dependencies The dependency addresses to check. - function externalCheckDependencies( - string[] memory networks, - address[] memory dependencies - ) external { + function externalCheckDependencies(string[] memory networks, address[] memory dependencies) external { LibRainDeploy.checkDependencies(vm, networks, dependencies, sDepCodeHashes); } @@ -116,7 +121,15 @@ contract LibRainDeployTest is Test { address[] memory dependencies ) external returns (address deployedAddress) { deployedAddress = LibRainDeploy.deployToNetworks( - vm, networks, deployer, creationCode, contractPath, expectedAddress, expectedCodeHash, dependencies, sDepCodeHashes + vm, + networks, + deployer, + creationCode, + contractPath, + expectedAddress, + expectedCodeHash, + dependencies, + sDepCodeHashes ); } @@ -152,7 +165,11 @@ contract LibRainDeployTest is Test { string[] memory networks = new string[](1); networks[0] = LibRainDeploy.ARBITRUM_ONE; vm.expectRevert( - abi.encodeWithSelector(LibRainDeploy.UnexpectedDeployedAddress.selector, address(0xdead), 0xC24016f209562fc151e5Ab7F88694ED5775feb36) + abi.encodeWithSelector( + LibRainDeploy.UnexpectedDeployedAddress.selector, + address(0xdead), + 0xC24016f209562fc151e5Ab7F88694ED5775feb36 + ) ); this.externalDeployToNetworks( networks, address(this), type(MockDeployable).creationCode, "", address(0xdead), bytes32(0), dependencies @@ -169,7 +186,11 @@ contract LibRainDeployTest is Test { address expectedAddress = 0xC24016f209562fc151e5Ab7F88694ED5775feb36; bytes32 wrongCodeHash = bytes32(uint256(1)); vm.expectRevert( - abi.encodeWithSelector(LibRainDeploy.UnexpectedDeployedCodeHash.selector, wrongCodeHash, 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483) + abi.encodeWithSelector( + LibRainDeploy.UnexpectedDeployedCodeHash.selector, + wrongCodeHash, + 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483 + ) ); this.externalDeployToNetworks( networks, address(this), type(MockDeployable).creationCode, "", expectedAddress, wrongCodeHash, dependencies @@ -216,7 +237,9 @@ contract LibRainDeployTest is Test { dependencies[0] = address(0xdead); vm.expectRevert( - abi.encodeWithSelector(LibRainDeploy.MissingDependency.selector, LibRainDeploy.ARBITRUM_ONE, address(0xdead)) + abi.encodeWithSelector( + LibRainDeploy.MissingDependency.selector, LibRainDeploy.ARBITRUM_ONE, address(0xdead) + ) ); this.externalCheckDependencies(networks, dependencies); } @@ -240,9 +263,7 @@ contract LibRainDeployTest is Test { // The actual codehash comes from the fork, so we cannot hardcode it. // Instead, just verify the revert happens with any DependencyChanged. vm.expectRevert(); - this.externalDeployToNetworks( - networks, address(this), hex"", "", address(0), bytes32(0), dependencies - ); + this.externalDeployToNetworks(networks, address(this), hex"", "", address(0), bytes32(0), dependencies); } /// `deployToNetworks` MUST revert with `DependencyChanged` when a @@ -260,8 +281,6 @@ contract LibRainDeployTest is Test { sDepCodeHashes[LibRainDeploy.ARBITRUM_ONE][address(0xdead)] = bytes32(uint256(1)); vm.expectRevert(); - this.externalDeployToNetworks( - networks, address(this), hex"", "", address(0), bytes32(0), dependencies - ); + this.externalDeployToNetworks(networks, address(this), hex"", "", address(0), bytes32(0), dependencies); } } From 072fa3dba0e70b2db3d66a8f2f2394be72e25580 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Thu, 5 Mar 2026 20:31:03 +0400 Subject: [PATCH 6/9] add RPC URL env vars to CI for fork tests Co-Authored-By: Claude Opus 4.6 --- .github/workflows/rainix.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/rainix.yaml b/.github/workflows/rainix.yaml index 4093c6d..5a47384 100644 --- a/.github/workflows/rainix.yaml +++ b/.github/workflows/rainix.yaml @@ -53,4 +53,6 @@ jobs: CI_FORK_SEPOLIA_BLOCK_NUMBER: ${{ vars.CI_FORK_SEPOLIA_BLOCK_NUMBER }} CI_FORK_SEPOLIA_DEPLOYER_ADDRESS: ${{ vars.CI_FORK_SEPOLIA_DEPLOYER_ADDRESS }} CI_DEPLOY_SEPOLIA_RPC_URL: ${{ secrets.CI_DEPLOY_SEPOLIA_RPC_URL || vars.CI_DEPLOY_SEPOLIA_RPC_URL }} + ARBITRUM_RPC_URL: ${{ secrets.RPC_URL_ARBITRUM_FORK || vars.RPC_URL_ARBITRUM_FORK }} + BASE_RPC_URL: ${{ secrets.RPC_URL_BASE_FORK || vars.RPC_URL_BASE_FORK }} run: nix develop -c ${{ matrix.task }} From dcee1160f3efd3a88c5dd964ddb06a9502c48c67 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Thu, 5 Mar 2026 20:38:45 +0400 Subject: [PATCH 7/9] address coderabbit review feedback - Verify Zoltu factory codehash in checkDependencies and deployToNetworks - Add flare and polygon RPC endpoints to foundry.toml - Replace bare vm.expectRevert() with exact error encoding - Update CLAUDE.md architecture docs and add RPC config section Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 16 +++++++++++++++- foundry.toml | 2 ++ src/lib/LibRainDeploy.sol | 10 +++++++++- test/src/lib/LibRainDeploy.t.sol | 22 ++++++++++++++++++---- 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index cfcdf04..4471f72 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,14 +35,28 @@ nix develop -c rainix-sol-legal CI runs three matrix tasks: `rainix-sol-legal`, `rainix-sol-test`, `rainix-sol-static`. +## RPC Configuration + +Fork tests require RPC endpoints defined in `.env` (gitignored): +``` +ARBITRUM_RPC_URL=https://arb1.arbitrum.io/rpc +BASE_RPC_URL=https://mainnet.base.org +FLARE_RPC_URL= +POLYGON_RPC_URL= +``` +These are referenced in `foundry.toml` under `[rpc_endpoints]`. + ## Architecture The entire library is a single file: `src/lib/LibRainDeploy.sol`. **LibRainDeploy** provides: +- `etchZoltuFactory(Vm)` — etches the Zoltu factory bytecode at the factory address (for networks where it isn't deployed) - `deployZoltu(bytes creationCode)` — deploys creation code via the Zoltu factory (`0x7A0D94F55792C434d74a40883C6ed8545E406D12`) using low-level `call`, returns the deployed address - `supportedNetworks()` — returns the list of Rain-supported network names (used as foundry RPC config aliases) -- `deployAndBroadcastToSupportedNetworks(...)` — the main entry point: forks each network, checks dependencies exist, deploys via Zoltu, verifies the deployed address and code hash match expectations +- `checkDependencies(...)` — forks each network, verifies dependencies and Zoltu factory exist with expected codehashes +- `deployToNetworks(...)` — re-verifies dependencies, deploys via Zoltu, verifies address and code hash +- `deployAndBroadcast(...)` — the main entry point: derives deployer from private key, calls `checkDependencies` then `deployToNetworks` The library is designed to be called from Foundry scripts (`forge script`) in consuming repos, not directly. Consuming repos provide their own creation code, expected addresses, expected code hashes, and dependency lists. diff --git a/foundry.toml b/foundry.toml index 2d382c1..c1b60e9 100644 --- a/foundry.toml +++ b/foundry.toml @@ -11,3 +11,5 @@ bytecode_hash = "none" [rpc_endpoints] arbitrum = "${ARBITRUM_RPC_URL}" base = "${BASE_RPC_URL}" +flare = "${FLARE_RPC_URL}" +polygon = "${POLYGON_RPC_URL}" diff --git a/src/lib/LibRainDeploy.sol b/src/lib/LibRainDeploy.sol index edac01a..377f3f3 100644 --- a/src/lib/LibRainDeploy.sol +++ b/src/lib/LibRainDeploy.sol @@ -112,10 +112,13 @@ library LibRainDeploy { console2.log("Checking dependencies on network:", networks[i]); console2.log(" - Zoltu Factory:", ZOLTU_FACTORY); - // Zoltu factory must exist always. + // Zoltu factory must exist with the expected codehash. if (ZOLTU_FACTORY.code.length == 0) { revert MissingDependency(networks[i], ZOLTU_FACTORY); } + if (ZOLTU_FACTORY.codehash != ZOLTU_FACTORY_CODEHASH) { + revert DependencyChanged(networks[i], ZOLTU_FACTORY, ZOLTU_FACTORY_CODEHASH, ZOLTU_FACTORY.codehash); + } for (uint256 j = 0; j < dependencies.length; j++) { console2.log(" - Dependency:", dependencies[j]); @@ -156,6 +159,11 @@ library LibRainDeploy { (forkId); console2.log("Block number:", block.number); + // Re-verify Zoltu factory codehash. + if (ZOLTU_FACTORY.code.length == 0 || ZOLTU_FACTORY.codehash != ZOLTU_FACTORY_CODEHASH) { + revert DependencyChanged(networks[i], ZOLTU_FACTORY, ZOLTU_FACTORY_CODEHASH, ZOLTU_FACTORY.codehash); + } + // Re-verify dependencies have not changed since the check phase. for (uint256 j = 0; j < dependencies.length; j++) { if ( diff --git a/test/src/lib/LibRainDeploy.t.sol b/test/src/lib/LibRainDeploy.t.sol index e85f5af..45d32db 100644 --- a/test/src/lib/LibRainDeploy.t.sol +++ b/test/src/lib/LibRainDeploy.t.sol @@ -260,9 +260,15 @@ contract LibRainDeployTest is Test { // the check and deploy phases. sDepCodeHashes[LibRainDeploy.ARBITRUM_ONE][LibRainDeploy.ZOLTU_FACTORY] = bytes32(uint256(1)); - // The actual codehash comes from the fork, so we cannot hardcode it. - // Instead, just verify the revert happens with any DependencyChanged. - vm.expectRevert(); + vm.expectRevert( + abi.encodeWithSelector( + LibRainDeploy.DependencyChanged.selector, + LibRainDeploy.ARBITRUM_ONE, + LibRainDeploy.ZOLTU_FACTORY, + bytes32(uint256(1)), + LibRainDeploy.ZOLTU_FACTORY_CODEHASH + ) + ); this.externalDeployToNetworks(networks, address(this), hex"", "", address(0), bytes32(0), dependencies); } @@ -280,7 +286,15 @@ contract LibRainDeployTest is Test { // Pre-populate as if the dependency existed during the check phase. sDepCodeHashes[LibRainDeploy.ARBITRUM_ONE][address(0xdead)] = bytes32(uint256(1)); - vm.expectRevert(); + vm.expectRevert( + abi.encodeWithSelector( + LibRainDeploy.DependencyChanged.selector, + LibRainDeploy.ARBITRUM_ONE, + address(0xdead), + bytes32(uint256(1)), + 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 + ) + ); this.externalDeployToNetworks(networks, address(this), hex"", "", address(0), bytes32(0), dependencies); } } From 47739378a6b20e4f59a200f5183a7ed5a00288e9 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Thu, 5 Mar 2026 21:27:49 +0400 Subject: [PATCH 8/9] audit 2026-03-05-02: 11 fixed, 1 documented - CI: add FLARE_RPC_URL and POLYGON_RPC_URL env vars - CLAUDE.md: replace placeholder RPC URLs with real endpoints - Add NoNetworks guard to checkDependencies and deployToNetworks - Split combined error checks in deployToNetworks to match checkDependencies pattern (MissingDependency vs DependencyChanged) - Fix NatSpec: "proxy" -> "factory", broaden DependencyChanged desc - Add assembly inline comments explaining memory layout - Document slither (forkId) suppression pattern - Add 9 new tests: NoNetworks guards, Zoltu factory verification paths, reverting constructor DeployFailed, skip-deployment path - Add .coderabbitai.yaml to exclude audit artifacts from review Co-Authored-By: Claude Opus 4.6 --- .coderabbitai.yaml | 7 + .github/workflows/rainix.yaml | 2 + CLAUDE.md | 4 +- audit/2026-03-05-02/pass0/process.md | 21 +++ audit/2026-03-05-02/pass1/LibRainDeploy.md | 96 ++++++++++++ audit/2026-03-05-02/pass2/LibRainDeploy.md | 165 +++++++++++++++++++++ audit/2026-03-05-02/pass3/LibRainDeploy.md | 61 ++++++++ audit/2026-03-05-02/pass4/LibRainDeploy.md | 130 ++++++++++++++++ audit/2026-03-05-02/pass5/LibRainDeploy.md | 151 +++++++++++++++++++ audit/2026-03-05-02/triage.md | 64 ++++++++ src/lib/LibRainDeploy.sol | 31 +++- test/src/lib/LibRainDeploy.t.sol | 161 +++++++++++++++++++- 12 files changed, 877 insertions(+), 16 deletions(-) create mode 100644 .coderabbitai.yaml create mode 100644 audit/2026-03-05-02/pass0/process.md create mode 100644 audit/2026-03-05-02/pass1/LibRainDeploy.md create mode 100644 audit/2026-03-05-02/pass2/LibRainDeploy.md create mode 100644 audit/2026-03-05-02/pass3/LibRainDeploy.md create mode 100644 audit/2026-03-05-02/pass4/LibRainDeploy.md create mode 100644 audit/2026-03-05-02/pass5/LibRainDeploy.md create mode 100644 audit/2026-03-05-02/triage.md diff --git a/.coderabbitai.yaml b/.coderabbitai.yaml new file mode 100644 index 0000000..63c3ccb --- /dev/null +++ b/.coderabbitai.yaml @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LicenseRef-DCL-1.0 +# SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd + +reviews: + path_filters: + - "!audit/**" + - "!.fixes/**" diff --git a/.github/workflows/rainix.yaml b/.github/workflows/rainix.yaml index 5a47384..7507589 100644 --- a/.github/workflows/rainix.yaml +++ b/.github/workflows/rainix.yaml @@ -55,4 +55,6 @@ jobs: CI_DEPLOY_SEPOLIA_RPC_URL: ${{ secrets.CI_DEPLOY_SEPOLIA_RPC_URL || vars.CI_DEPLOY_SEPOLIA_RPC_URL }} ARBITRUM_RPC_URL: ${{ secrets.RPC_URL_ARBITRUM_FORK || vars.RPC_URL_ARBITRUM_FORK }} BASE_RPC_URL: ${{ secrets.RPC_URL_BASE_FORK || vars.RPC_URL_BASE_FORK }} + FLARE_RPC_URL: ${{ secrets.RPC_URL_FLARE_FORK || vars.RPC_URL_FLARE_FORK }} + POLYGON_RPC_URL: ${{ secrets.RPC_URL_POLYGON_FORK || vars.RPC_URL_POLYGON_FORK }} run: nix develop -c ${{ matrix.task }} diff --git a/CLAUDE.md b/CLAUDE.md index 4471f72..8bd1bd4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,8 +41,8 @@ Fork tests require RPC endpoints defined in `.env` (gitignored): ``` ARBITRUM_RPC_URL=https://arb1.arbitrum.io/rpc BASE_RPC_URL=https://mainnet.base.org -FLARE_RPC_URL= -POLYGON_RPC_URL= +FLARE_RPC_URL=https://flare-api.flare.network/ext/C/rpc +POLYGON_RPC_URL=https://polygon-rpc.com ``` These are referenced in `foundry.toml` under `[rpc_endpoints]`. diff --git a/audit/2026-03-05-02/pass0/process.md b/audit/2026-03-05-02/pass0/process.md new file mode 100644 index 0000000..cda87aa --- /dev/null +++ b/audit/2026-03-05-02/pass0/process.md @@ -0,0 +1,21 @@ +# Pass 0: Process Review — 2026-03-05-02 + +## Documents Reviewed +- `CLAUDE.md` +- `foundry.toml` +- `slither.config.json` +- `.github/workflows/rainix.yaml` + +## Findings + +### A01-1 [LOW] CI missing FLARE_RPC_URL and POLYGON_RPC_URL env vars + +**Location:** `.github/workflows/rainix.yaml`, lines 56-57 + +The workflow passes `ARBITRUM_RPC_URL` and `BASE_RPC_URL` to the test step but omits `FLARE_RPC_URL` and `POLYGON_RPC_URL`. These are defined in `foundry.toml` (lines 14-15) and documented in `CLAUDE.md` (lines 44-45). Any future test that forks Flare or Polygon will fail in CI silently. + +### A01-2 [LOW] CLAUDE.md RPC section has placeholder values for Flare and Polygon + +**Location:** `CLAUDE.md`, lines 44-45 + +The RPC configuration section provides real URLs for Arbitrum and Base but uses `` and `` placeholders for Flare and Polygon. A future session copying these values verbatim into `.env` would get invalid URLs. Either provide real public RPC URLs or explicitly state these must be filled in by the developer. diff --git a/audit/2026-03-05-02/pass1/LibRainDeploy.md b/audit/2026-03-05-02/pass1/LibRainDeploy.md new file mode 100644 index 0000000..a4885bb --- /dev/null +++ b/audit/2026-03-05-02/pass1/LibRainDeploy.md @@ -0,0 +1,96 @@ +# Pass 1: Security — A01 — LibRainDeploy + +**Agent:** A01 +**File:** `src/lib/LibRainDeploy.sol` (253 lines) + +## Evidence of Thorough Reading + +**Library name:** `LibRainDeploy` (line 14) + +**Functions:** +| Function | Line | Visibility | +|----------|------|------------| +| `etchZoltuFactory(Vm vm)` | 60 | internal | +| `deployZoltu(bytes memory creationCode)` | 68 | internal | +| `supportedNetworks()` | 87 | internal pure | +| `checkDependencies(Vm, string[], address[], mapping(...))` | 102 | internal | +| `deployToNetworks(Vm, string[], address, bytes, string, address, bytes32, address[], mapping(...))` | 145 | internal | +| `deployAndBroadcast(Vm, string[], uint256, bytes, string, address, bytes32, address[], mapping(...))` | 222 | internal | + +**Errors:** +| Error | Line | +|-------|------| +| `DeployFailed(bool success, address deployedAddress)` | 18 | +| `MissingDependency(string network, address dependency)` | 21 | +| `UnexpectedDeployedAddress(address expected, address actual)` | 24 | +| `UnexpectedDeployedCodeHash(bytes32 expected, bytes32 actual)` | 27 | +| `DependencyChanged(string network, address dependency, bytes32 expectedCodeHash, bytes32 actualCodeHash)` | 31 | +| `NoNetworks()` | 34 | + +**Constants:** +| Constant | Line | Type | +|----------|------|------| +| `ZOLTU_FACTORY` | 37 | `address` | +| `ZOLTU_FACTORY_CODEHASH` | 40 | `bytes32` | +| `ZOLTU_FACTORY_BYTECODE` | 43 | `bytes` | +| `ARBITRUM_ONE` | 46 | `string` | +| `BASE` | 49 | `string` | +| `FLARE` | 52 | `string` | +| `POLYGON` | 55 | `string` | + +**Types:** None defined. + +**Imports:** `Vm` from `forge-std/Vm.sol` (line 5), `console2` from `forge-std/console2.sol` (line 6). + +## Security Checklist Analysis + +### Memory safety in assembly blocks + +The assembly block at lines 71-75 is annotated `"memory-safe"` and operates exclusively on scratch space (memory offsets 0-31). The sequence `mstore(0, 0)` zeroes 32 bytes, then the `call` return buffer writes 20 bytes at offset 12 (filling bytes 12-31), and `mload(0)` reads the full 32 bytes back as a properly left-padded address. The input pointer `add(creationCode, 0x20)` and length `mload(creationCode)` correctly dereference a `bytes memory` argument. No memory beyond scratch space is written, so the `memory-safe` annotation is valid. + +### Input validation + +`deployAndBroadcast` validates `networks.length == 0` (line 233). `deployZoltu` validates the call result, zero-address, and empty-code conditions (line 76). `deployToNetworks` verifies the deployed address and code hash post-deployment (lines 191-197). See finding A01-1 regarding `deployToNetworks` and `checkDependencies` lacking their own empty-networks guard. + +### Reentrancy and state consistency + +Not applicable. This is a library of `internal` functions used in Foundry scripts. There are no external entry points, no state variables, and no callbacks. The `call` on line 73 targets the Zoltu factory which executes CREATE and returns; it does not call back into the deployer. + +### Arithmetic safety + +No arithmetic operations beyond loop counters incrementing from 0 to `networks.length` and `dependencies.length`. Overflow is not possible for realistic array lengths under Solidity 0.8.x checked arithmetic. + +### Error handling + +All error paths use custom errors (`DeployFailed`, `MissingDependency`, `UnexpectedDeployedAddress`, `UnexpectedDeployedCodeHash`, `DependencyChanged`, `NoNetworks`). No string reverts or `require` statements found. + +### Cryptographic issues + +The library relies on `codehash` (EXTCODEHASH opcode, keccak256 of runtime bytecode) for integrity verification. This is the standard EVM mechanism and is not vulnerable to known attacks. + +### Resource management + +Fork creation via `vm.createSelectFork` is a Foundry cheatcode. Fork IDs are obtained but intentionally unused (suppressed with `(forkId);` on lines 110 and 159). Each fork remains active in Foundry's state until the script ends. This is standard Foundry usage with no leak risk. + +### Bytecode hash verification + +`ZOLTU_FACTORY_CODEHASH` (line 40) is checked against the on-chain `codehash` in both `checkDependencies` (line 119) and `deployToNetworks` (line 163). `ZOLTU_FACTORY_BYTECODE` (line 43) is used only in `etchZoltuFactory` for local test environments. Consistency between `ZOLTU_FACTORY_BYTECODE` and `ZOLTU_FACTORY_CODEHASH` is verified by existing tests (`testZoltuFactoryBytecodeMatchesOnChain` and `testEtchZoltuFactory`). + +## Findings + +### A01-1 [LOW] `deployToNetworks` and `checkDependencies` lack empty-networks guard + +**Location:** `src/lib/LibRainDeploy.sol`:102, `src/lib/LibRainDeploy.sol`:145 + +**Description:** +`deployAndBroadcast` (line 233) reverts with `NoNetworks()` when `networks.length == 0`, but neither `checkDependencies` nor `deployToNetworks` performs this check. Both are `internal` functions that could be called directly by consuming contracts that do not route through `deployAndBroadcast`. + +If `deployToNetworks` is called with an empty `networks` array, the for-loop on line 156 is skipped entirely and `deployedAddress` defaults to `address(0)`, which is returned as a successful result. A consumer relying on the returned address would proceed with `address(0)` as if it were a valid deployment. + +Similarly, `checkDependencies` with an empty array silently succeeds, recording no dependency hashes, which would cause `deployToNetworks` to accept any codehash for dependencies (since the mapping returns `bytes32(0)` for unset keys -- though this would then fail because no real contract has codehash `bytes32(0)`). + +**Impact:** A consuming contract that calls `deployToNetworks` directly with an empty networks array receives `address(0)` as a valid deployed address. Defense-in-depth is weakened by not validating at each function boundary. + +--- + +No CRITICAL, HIGH, or MEDIUM findings identified. diff --git a/audit/2026-03-05-02/pass2/LibRainDeploy.md b/audit/2026-03-05-02/pass2/LibRainDeploy.md new file mode 100644 index 0000000..5be4193 --- /dev/null +++ b/audit/2026-03-05-02/pass2/LibRainDeploy.md @@ -0,0 +1,165 @@ +# Pass 2: Test Coverage -- A01 -- LibRainDeploy + +**Agent:** A01 +**Source file:** `src/lib/LibRainDeploy.sol` (253 lines) +**Test file:** `test/src/lib/LibRainDeploy.t.sol` (300 lines) + +## Evidence of Thorough Reading + +### Source: `LibRainDeploy` (library, line 14) + +**Functions:** +| Function | Line | Visibility | +|----------|------|------------| +| `etchZoltuFactory(Vm vm)` | 60 | internal | +| `deployZoltu(bytes memory creationCode)` | 68 | internal | +| `supportedNetworks()` | 87 | internal pure | +| `checkDependencies(Vm, string[], address[], mapping(...))` | 102 | internal | +| `deployToNetworks(Vm, string[], address, bytes, string, address, bytes32, address[], mapping(...))` | 145 | internal | +| `deployAndBroadcast(Vm, string[], uint256, bytes, string, address, bytes32, address[], mapping(...))` | 222 | internal | + +**Errors:** +| Error | Line | +|-------|------| +| `DeployFailed(bool success, address deployedAddress)` | 18 | +| `MissingDependency(string network, address dependency)` | 21 | +| `UnexpectedDeployedAddress(address expected, address actual)` | 24 | +| `UnexpectedDeployedCodeHash(bytes32 expected, bytes32 actual)` | 27 | +| `DependencyChanged(string network, address dependency, bytes32 expectedCodeHash, bytes32 actualCodeHash)` | 31 | +| `NoNetworks()` | 34 | + +**Constants:** +| Constant | Line | Type | +|----------|------|------| +| `ZOLTU_FACTORY` | 37 | `address` | +| `ZOLTU_FACTORY_CODEHASH` | 40 | `bytes32` | +| `ZOLTU_FACTORY_BYTECODE` | 43 | `bytes` | +| `ARBITRUM_ONE` | 46 | `string` | +| `BASE` | 49 | `string` | +| `FLARE` | 52 | `string` | +| `POLYGON` | 55 | `string` | + +**Types:** None defined. + +### Test: `LibRainDeployTest` (contract, line 19) with `MockDeployable` (line 10) + +**Tests:** +| Test | Line | +|------|------| +| `testSupportedNetworks()` | 24 | +| `testZoltuFactoryCodehash()` | 35 | +| `testZoltuFactoryBytecode()` | 42 | +| `testEtchZoltuFactory()` | 49 | +| `testNoNetworksReverts()` | 89 | +| `testDeployZoltu()` | 145 | +| `testDeployZoltuRevertsNoFactory()` | 153 | +| `testUnexpectedDeployedAddressReverts()` | 162 | +| `testUnexpectedDeployedCodeHashReverts()` | 181 | +| `testDeployAndBroadcastHappyPath()` | 202 | +| `testCheckDependenciesRecordsCodehash()` | 220 | +| `testMissingDependencyReverts()` | 233 | +| `testDependencyChangedCodehashReverts()` | 250 | +| `testDependencyChangedCodeLengthReverts()` | 277 | + +**External wrappers:** +| Wrapper | Line | +|---------|------| +| `externalDeployAndBroadcast(...)` | 66 | +| `externalCheckDependencies(...)` | 100 | +| `externalDeployToNetworks(...)` | 114 | +| `externalDeployZoltu(...)` | 139 | + +**Mock contracts:** +| Contract | Line | Purpose | +|----------|------|---------| +| `MockDeployable` | 10 | Minimal contract with `value = 42` for Zoltu deployment tests | + +**Storage:** +| Variable | Line | Type | +|----------|------|------| +| `sDepCodeHashes` | 20 | `mapping(string => mapping(address => bytes32))` | + +### Indirect Coverage Search + +Grepping for all source function names and error names across `/test` found references only in `test/src/lib/LibRainDeploy.t.sol`. No other test files reference `LibRainDeploy`. + +## Coverage Matrix + +| Source Function / Error Path | Test(s) | Covered? | +|------------------------------|---------|----------| +| `etchZoltuFactory` happy path | `testEtchZoltuFactory` | YES | +| `deployZoltu` happy path | `testDeployZoltu` | YES | +| `deployZoltu` -- `DeployFailed` (success=true, addr=0) | `testDeployZoltuRevertsNoFactory` | YES | +| `deployZoltu` -- `DeployFailed` (success=false) | *none* | **NO** | +| `supportedNetworks` | `testSupportedNetworks` | YES | +| `checkDependencies` happy path | `testCheckDependenciesRecordsCodehash` | YES | +| `checkDependencies` -- `MissingDependency` (user dep) | `testMissingDependencyReverts` | YES | +| `checkDependencies` -- `MissingDependency` (Zoltu factory) | *none* | **NO** | +| `checkDependencies` -- `DependencyChanged` (Zoltu codehash) | *none* | **NO** | +| `deployToNetworks` happy path | `testDeployAndBroadcastHappyPath` (indirectly) | YES | +| `deployToNetworks` -- `UnexpectedDeployedAddress` | `testUnexpectedDeployedAddressReverts` | YES | +| `deployToNetworks` -- `UnexpectedDeployedCodeHash` | `testUnexpectedDeployedCodeHashReverts` | YES | +| `deployToNetworks` -- `DependencyChanged` (dep codehash) | `testDependencyChangedCodehashReverts` | YES | +| `deployToNetworks` -- `DependencyChanged` (dep code.length=0) | `testDependencyChangedCodeLengthReverts` | YES | +| `deployToNetworks` -- `DependencyChanged` (Zoltu factory) | *none* | **NO** | +| `deployToNetworks` -- skip deployment (code already exists) | *none* | **NO** | +| `deployAndBroadcast` happy path | `testDeployAndBroadcastHappyPath` | YES | +| `deployAndBroadcast` -- `NoNetworks` | `testNoNetworksReverts` | YES | +| Constants verified on-chain | `testZoltuFactoryCodehash`, `testZoltuFactoryBytecode` | YES | + +## Findings + +### P2-A01-1 [MEDIUM] `checkDependencies` -- `MissingDependency` for Zoltu factory is untested + +**Location:** `src/lib/LibRainDeploy.sol`:116-118 + +**Description:** +Lines 116-118 revert with `MissingDependency(networks[i], ZOLTU_FACTORY)` when the Zoltu factory has no code on a network. No test exercises this path. The existing `testMissingDependencyReverts` only tests a user-provided dependency (`address(0xdead)`) missing code, not the Zoltu factory itself being absent. + +This is a distinct code path from any dependency check: the Zoltu factory check happens before the dependency loop (lines 116-121 vs. lines 123-129), and uses a hardcoded address constant. + +--- + +### P2-A01-2 [MEDIUM] `checkDependencies` -- `DependencyChanged` for Zoltu factory codehash mismatch is untested + +**Location:** `src/lib/LibRainDeploy.sol`:119-121 + +**Description:** +Lines 119-121 revert with `DependencyChanged(networks[i], ZOLTU_FACTORY, ZOLTU_FACTORY_CODEHASH, ZOLTU_FACTORY.codehash)` when the Zoltu factory exists but has a wrong codehash. No test exercises this path through `checkDependencies`. + +The existing `testDependencyChangedCodehashReverts` triggers the analogous check in `deployToNetworks` (lines 163-165) for a user dependency, not for the Zoltu factory, and not via `checkDependencies`. + +--- + +### P2-A01-3 [MEDIUM] `deployToNetworks` -- `DependencyChanged` for Zoltu factory is untested + +**Location:** `src/lib/LibRainDeploy.sol`:163-165 + +**Description:** +Lines 163-165 revert with `DependencyChanged(...)` when the Zoltu factory has no code or a wrong codehash at deploy time. No test exercises this specific path. The existing `testDependencyChangedCodehashReverts` tests a user *dependency* codehash mismatch (lines 168-180), not the Zoltu factory re-verification. + +There are two sub-conditions: +1. `ZOLTU_FACTORY.code.length == 0` -- factory destroyed between check and deploy +2. `ZOLTU_FACTORY.codehash != ZOLTU_FACTORY_CODEHASH` -- factory replaced between check and deploy + +Neither is tested. + +--- + +### P2-A01-4 [LOW] `deployZoltu` -- `DeployFailed` with `success=false` is untested + +**Location:** `src/lib/LibRainDeploy.sol`:76 + +**Description:** +The revert condition on line 76 includes `!success`, covering the case where the low-level `call` to the Zoltu factory itself reverts (e.g., creation code with a reverting constructor). The existing `testDeployZoltuRevertsNoFactory` only triggers `DeployFailed(true, address(0))` by etching the factory to empty bytecode, which makes the call succeed (no code = no revert) but return zero. No test triggers the `success=false` branch where the inner CREATE causes the factory call to revert. + +--- + +### P2-A01-5 [LOW] `deployToNetworks` -- skip-deployment path is untested + +**Location:** `src/lib/LibRainDeploy.sol`:183-189 + +**Description:** +When `expectedAddress.code.length > 0` (line 183), deployment is skipped and `deployedAddress` is set to `expectedAddress` (line 188). This "already deployed" branch is never exercised by any test. All existing deploy tests start from a clean state where the expected address has no code. + +This path has production relevance: when re-running deployments across multiple networks, some networks may already have the contract. If the skip logic has a bug (e.g., not verifying the codehash of the pre-existing code), it would go undetected. diff --git a/audit/2026-03-05-02/pass3/LibRainDeploy.md b/audit/2026-03-05-02/pass3/LibRainDeploy.md new file mode 100644 index 0000000..fe07a75 --- /dev/null +++ b/audit/2026-03-05-02/pass3/LibRainDeploy.md @@ -0,0 +1,61 @@ +# Pass 3 (Documentation) - LibRainDeploy.sol + +**Agent:** A01 +**File:** `/Users/thedavidmeister/Code/rain.deploy/src/lib/LibRainDeploy.sol` + +## Evidence of Thorough Reading + +**Library:** `LibRainDeploy` (line 14) + +**Functions:** +1. `etchZoltuFactory(Vm vm)` -- line 60 +2. `deployZoltu(bytes memory creationCode)` -- line 68 +3. `supportedNetworks()` -- line 87 +4. `checkDependencies(Vm, string[], address[], mapping)` -- line 102 +5. `deployToNetworks(Vm, string[], address, bytes, string, address, bytes32, address[], mapping)` -- line 145 +6. `deployAndBroadcast(Vm, string[], uint256, bytes, string, address, bytes32, address[], mapping)` -- line 222 + +**Errors:** +1. `DeployFailed(bool, address)` -- line 18 +2. `MissingDependency(string, address)` -- line 21 +3. `UnexpectedDeployedAddress(address, address)` -- line 24 +4. `UnexpectedDeployedCodeHash(bytes32, bytes32)` -- line 27 +5. `DependencyChanged(string, address, bytes32, bytes32)` -- line 31 +6. `NoNetworks()` -- line 34 + +**Constants:** +1. `ZOLTU_FACTORY` (address) -- line 37 +2. `ZOLTU_FACTORY_CODEHASH` (bytes32) -- line 40 +3. `ZOLTU_FACTORY_BYTECODE` (bytes) -- line 43 +4. `ARBITRUM_ONE` (string) -- line 46 +5. `BASE` (string) -- line 49 +6. `FLARE` (string) -- line 52 +7. `POLYGON` (string) -- line 55 + +**Types:** None. + +## Documentation Completeness + +All 6 functions have NatSpec documentation with `@param` and `@return` tags. +All 6 errors have NatSpec documentation. +All 7 constants have NatSpec documentation. + +## Findings + +### A01-1: ZOLTU_FACTORY NatSpec calls it a "proxy" instead of "factory" + +- **Severity:** LOW +- **Location:** `src/lib/LibRainDeploy.sol:36` +- **Description:** The NatSpec for the `ZOLTU_FACTORY` constant reads "Zoltu proxy is the same on every network." The word "proxy" is inaccurate -- the constant is named `ZOLTU_FACTORY`, the library title refers to it as a "factory", and the Zoltu contract is a deterministic deployment factory, not a proxy. The NatSpec should say "factory" instead of "proxy" to avoid confusion. + +### A01-2: DependencyChanged NatSpec is narrower than actual usage + +- **Severity:** LOW +- **Location:** `src/lib/LibRainDeploy.sol:29-31` +- **Description:** The NatSpec for `DependencyChanged` reads: "Thrown when a dependency's code hash or size changed between the dependency check and the deployment." This implies the error is only thrown during re-verification in `deployToNetworks`. However, the error is also thrown during the initial `checkDependencies` phase (line 120) when the Zoltu factory exists but has an unexpected codehash -- this is not "between the dependency check and the deployment" but during the check itself. The NatSpec should describe the error more broadly, e.g., "Thrown when a dependency's code hash does not match the expected value." + +### A01-3: checkDependencies NatSpec understates Zoltu factory verification + +- **Severity:** INFO +- **Location:** `src/lib/LibRainDeploy.sol:96-97` +- **Description:** The NatSpec for `checkDependencies` says "Checks that the Zoltu factory and all dependencies have code on each network." This understates what the function does for the Zoltu factory specifically: it also verifies the factory's codehash matches the expected `ZOLTU_FACTORY_CODEHASH` constant (line 119), not merely that code exists. A more complete description would be "Checks that the Zoltu factory has the expected codehash and all dependencies have code on each network." diff --git a/audit/2026-03-05-02/pass4/LibRainDeploy.md b/audit/2026-03-05-02/pass4/LibRainDeploy.md new file mode 100644 index 0000000..6d16709 --- /dev/null +++ b/audit/2026-03-05-02/pass4/LibRainDeploy.md @@ -0,0 +1,130 @@ +# Pass 4 (Code Quality) - LibRainDeploy.sol + +**Agent:** A01 +**File:** `/Users/thedavidmeister/Code/rain.deploy/src/lib/LibRainDeploy.sol` (253 lines) + +## Evidence of Thorough Reading + +**Library:** `LibRainDeploy` (line 14) + +**Functions:** + +| Function | Line | Visibility | +|----------|------|------------| +| `etchZoltuFactory(Vm vm)` | 60 | internal | +| `deployZoltu(bytes memory creationCode)` | 68 | internal | +| `supportedNetworks()` | 87 | internal pure | +| `checkDependencies(Vm, string[], address[], mapping)` | 102 | internal | +| `deployToNetworks(Vm, string[], address, bytes, string, address, bytes32, address[], mapping)` | 145 | internal | +| `deployAndBroadcast(Vm, string[], uint256, bytes, string, address, bytes32, address[], mapping)` | 222 | internal | + +**Errors:** + +| Error | Line | +|-------|------| +| `DeployFailed(bool success, address deployedAddress)` | 18 | +| `MissingDependency(string network, address dependency)` | 21 | +| `UnexpectedDeployedAddress(address expected, address actual)` | 24 | +| `UnexpectedDeployedCodeHash(bytes32 expected, bytes32 actual)` | 27 | +| `DependencyChanged(string network, address dependency, bytes32 expectedCodeHash, bytes32 actualCodeHash)` | 31 | +| `NoNetworks()` | 34 | + +**Constants:** + +| Constant | Line | Type | +|----------|------|------| +| `ZOLTU_FACTORY` | 37 | address | +| `ZOLTU_FACTORY_CODEHASH` | 40 | bytes32 | +| `ZOLTU_FACTORY_BYTECODE` | 43 | bytes | +| `ARBITRUM_ONE` | 46 | string | +| `BASE` | 49 | string | +| `FLARE` | 52 | string | +| `POLYGON` | 55 | string | + +**Types:** None defined. + +**Imports:** `Vm` from `forge-std/Vm.sol` (line 5), `console2` from `forge-std/console2.sol` (line 6). + +## Findings + +### A01-1 [LOW] `(forkId);` no-op pattern to suppress unused variable warning is fragile and non-idiomatic + +**Location:** `src/lib/LibRainDeploy.sol:110`, `src/lib/LibRainDeploy.sol:159` + +**Description:** +Lines 110 and 159 both use the pattern: +```solidity +uint256 forkId = vm.createSelectFork(networks[i]); +(forkId); +``` + +The `(forkId);` statement is a parenthesized expression statement that evaluates the variable and discards the result. Its sole purpose is to suppress the Solidity compiler warning about an unused local variable. This pattern has several problems: + +1. **Non-idiomatic:** The standard Solidity idiom for discarding a return value is to omit the variable assignment entirely: `vm.createSelectFork(networks[i]);`. Since the return value of `createSelectFork` is not needed (the fork is selected as a side effect), there is no reason to capture it at all. + +2. **Misleading to readers:** The `(forkId);` line looks like an accidental leftover or a bug -- it suggests `forkId` was once used for something and the usage was removed but the declaration was not cleaned up. + +3. **Fragile across compiler versions:** While this works in current Solidity versions, expression statements with no side effects could become a compiler warning or error in future versions. + +The fix is simply to remove the variable assignment and the no-op line, calling `vm.createSelectFork(networks[i]);` directly. + +### A01-2 [LOW] Inconsistent error handling between `checkDependencies` and `deployToNetworks` for the same Zoltu factory verification + +**Location:** `src/lib/LibRainDeploy.sol:116-121` vs `src/lib/LibRainDeploy.sol:163-165` + +**Description:** +`checkDependencies` (lines 116-121) performs two separate checks for the Zoltu factory with two different error types: +```solidity +if (ZOLTU_FACTORY.code.length == 0) { + revert MissingDependency(networks[i], ZOLTU_FACTORY); +} +if (ZOLTU_FACTORY.codehash != ZOLTU_FACTORY_CODEHASH) { + revert DependencyChanged(networks[i], ZOLTU_FACTORY, ZOLTU_FACTORY_CODEHASH, ZOLTU_FACTORY.codehash); +} +``` + +`deployToNetworks` (lines 163-165) combines both checks with `||` into one condition, always reverting with `DependencyChanged`: +```solidity +if (ZOLTU_FACTORY.code.length == 0 || ZOLTU_FACTORY.codehash != ZOLTU_FACTORY_CODEHASH) { + revert DependencyChanged(networks[i], ZOLTU_FACTORY, ZOLTU_FACTORY_CODEHASH, ZOLTU_FACTORY.codehash); +} +``` + +This inconsistency means: +- In `checkDependencies`, a missing Zoltu factory (no code at all) produces `MissingDependency`, which is semantically precise. +- In `deployToNetworks`, a missing Zoltu factory produces `DependencyChanged`, which is misleading -- the dependency was not "changed", it is absent. Furthermore, when `code.length == 0`, `codehash` is `bytes32(0)`, so the `DependencyChanged` error will report `actualCodeHash` as zero, which is a confusing proxy for "no code exists." + +The same inconsistency also applies to dependency verification: `checkDependencies` (line 125) checks only `code.length == 0` and reverts with `MissingDependency`, while `deployToNetworks` (lines 169-171) combines `code.length == 0 || codehash != ...` and reverts with `DependencyChanged` for both cases. + +Both paths should use the same two-step pattern: first check for missing code (revert `MissingDependency`), then check for wrong codehash (revert `DependencyChanged`). This provides callers with precise, actionable error information. + +### A01-3 [LOW] Magic numbers `12` and `20` in assembly block lack inline documentation + +**Location:** `src/lib/LibRainDeploy.sol:73` + +**Description:** +```solidity +success := call(gas(), zoltuFactory, 0, add(creationCode, 0x20), mload(creationCode), 12, 20) +``` + +The assembly `call` passes `12` as the return data offset and `20` as the return data length. These encode the ABI layout: the Zoltu factory returns a raw 20-byte address (not ABI-encoded), which is written starting at memory offset 12 so that after `mload(0)` the address is correctly right-aligned in the 32-byte word (since offset 0 was zeroed on line 72). + +These numbers are correct but should have inline comments explaining the derivation: `20` is the size of an EVM address, and `12 = 32 - 20` is the padding offset. Without this, a reviewer must reconstruct the memory layout reasoning from scratch. In a library with security-critical assembly, this is a non-trivial maintenance burden. + +### A01-4 [INFO] `supportedNetworks()` and its associated constants have no callers within the repository + +**Location:** `src/lib/LibRainDeploy.sol:46-55` (constants), `src/lib/LibRainDeploy.sol:87-94` (`supportedNetworks()`) + +**Description:** +The four network name constants (`ARBITRUM_ONE`, `BASE`, `FLARE`, `POLYGON`) are only referenced by `supportedNetworks()`, and `supportedNetworks()` itself has no call sites anywhere in this repository. A grep for `supportedNetworks` across all `.sol` files returns only the definition. + +As an `internal` function in a library meant for downstream consumption, it may be used externally. However, within this project, the function and its constants are dead code -- they cannot be validated by any test or script. If a network name constant drifts out of sync with `foundry.toml`'s `[rpc_endpoints]` (e.g., a rename or addition), there is no in-repo mechanism to catch it. + +### A01-5 [INFO] `console2` logging throughout library couples it to forge-std + +**Location:** `src/lib/LibRainDeploy.sol:6` (import), lines 77-80, 111-112, 114, 124, 157, 160, 184, 187, 190, 194, 200-204, 238 + +**Description:** +The library uses `console2` at 16 call sites. This is appropriate for a Foundry deployment script library -- `console2` calls are no-ops on-chain and provide valuable deployment-time logging. The usage is consistent (used throughout, not sporadically). + +However, this permanently couples the library to the forge-std dependency. Any downstream consumer must have forge-std available. This is an architectural characteristic worth noting, not a defect, since the library inherently depends on forge-std already (for the `Vm` type). Both imports serve the same domain: Foundry scripting infrastructure. diff --git a/audit/2026-03-05-02/pass5/LibRainDeploy.md b/audit/2026-03-05-02/pass5/LibRainDeploy.md new file mode 100644 index 0000000..27f7b41 --- /dev/null +++ b/audit/2026-03-05-02/pass5/LibRainDeploy.md @@ -0,0 +1,151 @@ +# Pass 5 - Correctness / Intent Verification: LibRainDeploy + +**Audit:** 2026-03-05-02 +**Agent:** A01 +**File:** `src/lib/LibRainDeploy.sol` +**Test file:** `test/src/lib/LibRainDeploy.t.sol` + +--- + +## Evidence of Thorough Reading + +### `src/lib/LibRainDeploy.sol` + +**Library:** `LibRainDeploy` (line 14) + +**Functions:** +| Function | Line | +|---|---| +| `etchZoltuFactory(Vm)` | 60 | +| `deployZoltu(bytes memory)` | 68 | +| `supportedNetworks()` | 87 | +| `checkDependencies(Vm, string[], address[], mapping)` | 102 | +| `deployToNetworks(Vm, string[], address, bytes, string, address, bytes32, address[], mapping)` | 145 | +| `deployAndBroadcast(Vm, string[], uint256, bytes, string, address, bytes32, address[], mapping)` | 222 | + +**Errors:** +| Error | Line | +|---|---| +| `DeployFailed(bool success, address deployedAddress)` | 18 | +| `MissingDependency(string network, address dependency)` | 21 | +| `UnexpectedDeployedAddress(address expected, address actual)` | 24 | +| `UnexpectedDeployedCodeHash(bytes32 expected, bytes32 actual)` | 27 | +| `DependencyChanged(string network, address dependency, bytes32 expectedCodeHash, bytes32 actualCodeHash)` | 31 | +| `NoNetworks()` | 34 | + +**Constants:** +| Constant | Line | Value | +|---|---|---| +| `ZOLTU_FACTORY` | 37 | `0x7A0D94F55792C434d74a40883C6ed8545E406D12` | +| `ZOLTU_FACTORY_CODEHASH` | 40 | `0x5acaad953250bec20933f7c72a25bb03bfa54767ebd3a750396276512c46a79c` | +| `ZOLTU_FACTORY_BYTECODE` | 43 | `hex"6000368182..."` (30 bytes) | +| `ARBITRUM_ONE` | 46 | `"arbitrum"` | +| `BASE` | 49 | `"base"` | +| `FLARE` | 52 | `"flare"` | +| `POLYGON` | 55 | `"polygon"` | + +### `test/src/lib/LibRainDeploy.t.sol` + +**Contracts:** +- `MockDeployable` (line 10) - minimal deployment target with `uint256 public value = 42` +- `LibRainDeployTest is Test` (line 19) + +**State:** +- `sDepCodeHashes` (line 20) - `mapping(string => mapping(address => bytes32))` + +**Functions (test contract):** +| Function | Line | +|---|---| +| `testSupportedNetworks()` | 24 | +| `testZoltuFactoryCodehash()` | 35 | +| `testZoltuFactoryBytecode()` | 42 | +| `testEtchZoltuFactory()` | 49 | +| `externalDeployAndBroadcast(...)` | 66 | +| `testNoNetworksReverts()` | 89 | +| `externalCheckDependencies(...)` | 100 | +| `externalDeployToNetworks(...)` | 114 | +| `externalDeployZoltu(...)` | 139 | +| `testDeployZoltu()` | 145 | +| `testDeployZoltuRevertsNoFactory()` | 153 | +| `testUnexpectedDeployedAddressReverts()` | 162 | +| `testUnexpectedDeployedCodeHashReverts()` | 181 | +| `testDeployAndBroadcastHappyPath()` | 202 | +| `testCheckDependenciesRecordsCodehash()` | 220 | +| `testMissingDependencyReverts()` | 233 | +| `testDependencyChangedCodehashReverts()` | 250 | +| `testDependencyChangedCodeLengthReverts()` | 277 | + +--- + +## Verification Results + +### Constants and Magic Numbers + +1. **`ZOLTU_FACTORY_CODEHASH`**: Independently computed `keccak256(ZOLTU_FACTORY_BYTECODE)` = `0x5acaad953250bec20933f7c72a25bb03bfa54767ebd3a750396276512c46a79c`. Matches the constant at line 40. **Correct.** + +2. **`ZOLTU_FACTORY_BYTECODE`**: Disassembled and traced the bytecode. It implements: + - Copy all calldata to memory at offset 0 + - CREATE2 with salt=0, value=callvalue, creation code from memory + - On failure: REVERT with empty data + - On success: MSTORE address at offset 0, RETURN(offset=12, size=20) returning the 20-byte address + This is the standard Zoltu deterministic deployment proxy pattern. **Correct.** + +3. **Empty account codehash** `0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470` used in `testDependencyChangedCodeLengthReverts` (line 295): Independently computed `keccak256("")` matches. This is the codehash of an existing account with no code (not a non-existent account, which returns `bytes32(0)`). The test uses `address(0xdead)` on Arbitrum fork, which is expected to exist (have balance). **Correct.** + +### Assembly Correctness (`deployZoltu`, lines 71-75) + +Traced the assembly step by step: + +- `mstore(0, 0)`: Zeroes scratch space `memory[0:32]`. **Correct.** +- `call(gas(), zoltuFactory, 0, add(creationCode, 0x20), mload(creationCode), 12, 20)`: + - Input: `memory[creationCode+0x20]` for `mload(creationCode)` bytes (skips ABI length prefix, passes raw bytes). **Correct.** + - Output: writes 20 bytes of return data to `memory[12:32]`. **Correct.** +- `mload(0)`: Reads `memory[0:32]` as `uint256`. Bytes 0-11 are zero (from `mstore(0,0)`), bytes 12-31 contain the 20-byte address. The address is right-aligned in the 32-byte word, which is the correct representation for Solidity's `address` type. **Correct.** +- `memory-safe` annotation: All memory access is within the scratch space `[0x00, 0x40)`. The `creationCode` read is from Solidity-allocated memory. **Correct.** + +### NatSpec vs Implementation + +- `deployZoltu`: NatSpec says "Deploys the given creation code via the Zoltu factory. Handles the return data and errors appropriately." Implementation matches. **Correct.** +- `checkDependencies`: NatSpec says "Checks that the Zoltu factory and all dependencies have code on each network. Records each dependency's codehash in the provided mapping." Implementation matches. **Correct.** +- `supportedNetworks`: NatSpec says "Returns the list of networks currently supported by Rain deployments." Implementation returns 4 networks. **Correct.** +- `etchZoltuFactory`: NatSpec says "Etches the Zoltu factory bytecode into the factory address." Implementation calls `vm.etch`. **Correct.** +- `deployAndBroadcast`: NatSpec accurately describes the full flow. **Correct.** +- `deployToNetworks`: NatSpec says "Verifies that dependencies have not changed since the check phase, then deploys to each network via the Zoltu factory." See INFO finding below. + +### Error Conditions vs Triggers + +- `DeployFailed`: Triggered when `!success || deployedAddress == address(0) || deployedAddress.code.length == 0` (line 76). Matches the error description "deployment via Zoltu factory fails". **Correct.** +- `MissingDependency`: Triggered when `dependencies[j].code.length == 0` (line 125) or factory code missing (line 116-118). Matches description. **Correct.** +- `UnexpectedDeployedAddress`: Triggered when `deployedAddress != expectedAddress` (line 191). Matches description. **Correct.** +- `UnexpectedDeployedCodeHash`: Triggered when `expectedCodeHash != deployedAddress.codehash` (line 195). Matches description. **Correct.** +- `DependencyChanged`: Triggered when dependency codehash differs from recorded value (line 169-179) or factory codehash differs (line 163-165). Matches description. **Correct.** +- `NoNetworks`: Triggered when `networks.length == 0` (line 233). Matches description. **Correct.** + +### Tests vs Claims + +- `testSupportedNetworks`: Asserts 4 networks in correct order. **Matches claim.** +- `testZoltuFactoryCodehash`: Forks Arbitrum and asserts on-chain codehash matches constant. **Matches claim.** +- `testZoltuFactoryBytecode`: Forks Arbitrum and asserts on-chain bytecode matches constant. **Matches claim.** +- `testEtchZoltuFactory`: Verifies bytecode and codehash after etching. **Matches claim.** +- `testDeployZoltu`: Deploys MockDeployable via Zoltu on Arbitrum fork, asserts deterministic address `0xC24016f209562fc151e5Ab7F88694ED5775feb36`. **Matches claim.** +- `testDeployZoltuRevertsNoFactory`: Etches empty code at factory, expects `DeployFailed(true, address(0))`. When calling an empty account, EVM returns success with no data, so `deployedAddress` remains 0. **Matches claim.** +- `testNoNetworksReverts`: Empty array triggers `NoNetworks`. **Matches claim.** +- `testUnexpectedDeployedAddressReverts`: Passes wrong expected address, triggers `UnexpectedDeployedAddress`. **Matches claim.** +- `testUnexpectedDeployedCodeHashReverts`: Passes wrong expected codehash, triggers `UnexpectedDeployedCodeHash`. **Matches claim.** +- `testDeployAndBroadcastHappyPath`: Full end-to-end deploy, verifies correct address and codehash. **Matches claim.** +- `testCheckDependenciesRecordsCodehash`: Verifies storage mapping is populated. **Matches claim.** +- `testMissingDependencyReverts`: Uses `address(0xdead)` with no code, triggers `MissingDependency`. **Matches claim.** +- `testDependencyChangedCodehashReverts`: Pre-populates wrong codehash, triggers `DependencyChanged`. **Matches claim.** +- `testDependencyChangedCodeLengthReverts`: Pre-populates codehash for non-existent code at `0xdead`, triggers `DependencyChanged` with `keccak256("")` as actual codehash. **Matches claim.** + +--- + +## Findings + +### A01-1 [INFO] `deployToNetworks` NatSpec omits skip-if-already-deployed behavior + +**Location:** `src/lib/LibRainDeploy.sol:133-134` + +**Description:** The NatSpec for `deployToNetworks` says "Verifies that dependencies have not changed since the check phase, then deploys to each network via the Zoltu factory." However, the implementation (lines 183-189) skips deployment when `expectedAddress.code.length != 0` and returns the expected address directly. This skip-if-already-deployed behavior is documented in an inline comment (line 187) but not in the function-level NatSpec. A caller reading only the NatSpec would not know that the function is idempotent. + +No fix file generated for INFO-level finding. diff --git a/audit/2026-03-05-02/triage.md b/audit/2026-03-05-02/triage.md new file mode 100644 index 0000000..ffd9a5d --- /dev/null +++ b/audit/2026-03-05-02/triage.md @@ -0,0 +1,64 @@ +# Audit Triage — 2026-03-05-02 + +## Prior Audit + +Previous audit `2026-03-05-01` fully triaged (10 FIXED, 1 DISMISSED). Current P1-A01-1 is a refinement of previous P1-A01-1 (which added `NoNetworks()` to `deployAndBroadcast` but not to sub-functions). No other duplicates. + +## Findings Index (LOW+ only, sorted by pass then agent ID) + +| ID | Pass | Severity | Title | Status | +|----|------|----------|-------|--------| +| P0-A01-1 | 0 | LOW | CI missing FLARE_RPC_URL and POLYGON_RPC_URL env vars | FIXED | +| P0-A01-2 | 0 | LOW | CLAUDE.md RPC section has placeholder values for Flare and Polygon | FIXED | +| P1-A01-1 | 1 | LOW | `deployToNetworks` and `checkDependencies` lack empty-networks guard | FIXED | +| P2-A01-1 | 2 | MEDIUM | `checkDependencies` -- `MissingDependency` for Zoltu factory is untested | FIXED | +| P2-A01-2 | 2 | MEDIUM | `checkDependencies` -- `DependencyChanged` for Zoltu factory codehash mismatch is untested | FIXED | +| P2-A01-3 | 2 | MEDIUM | `deployToNetworks` -- `DependencyChanged` for Zoltu factory is untested | FIXED | +| P2-A01-4 | 2 | LOW | `deployZoltu` -- `DeployFailed` with `success=false` is untested | FIXED | +| P2-A01-5 | 2 | LOW | `deployToNetworks` -- skip-deployment path is untested | FIXED | +| P3-A01-1 | 3 | LOW | ZOLTU_FACTORY NatSpec calls it a "proxy" instead of "factory" | FIXED | +| P3-A01-2 | 3 | LOW | DependencyChanged NatSpec is narrower than actual usage | FIXED | +| P4-A01-1 | 4 | LOW | `(forkId);` no-op pattern to suppress unused variable warning | DOCUMENTED | +| P4-A01-2 | 4 | LOW | Inconsistent error handling between `checkDependencies` and `deployToNetworks` | FIXED | +| P4-A01-3 | 4 | LOW | Magic numbers `12` and `20` in assembly block lack inline documentation | FIXED | + +## Triage Decisions + +### P0-A01-1 — CI missing FLARE_RPC_URL and POLYGON_RPC_URL env vars +**Status:** FIXED — Added `FLARE_RPC_URL` and `POLYGON_RPC_URL` env vars to `.github/workflows/rainix.yaml`. + +### P0-A01-2 — CLAUDE.md RPC section has placeholder values for Flare and Polygon +**Status:** FIXED — Replaced placeholders with real public RPC URLs (`https://flare-api.flare.network/ext/C/rpc` and `https://polygon-rpc.com`). + +### P1-A01-1 — `deployToNetworks` and `checkDependencies` lack empty-networks guard +**Status:** FIXED — Added `NoNetworks()` guard to both `checkDependencies` and `deployToNetworks`. Added tests `testCheckDependenciesNoNetworksReverts` and `testDeployToNetworksNoNetworksReverts`. + +### P2-A01-1 — `checkDependencies` -- `MissingDependency` for Zoltu factory is untested +**Status:** FIXED — Added `testCheckDependenciesMissingZoltuFactoryReverts` which etches factory to empty (with `makePersistent`) and expects `MissingDependency(ARBITRUM_ONE, ZOLTU_FACTORY)`. + +### P2-A01-2 — `checkDependencies` -- `DependencyChanged` for Zoltu factory codehash mismatch is untested +**Status:** FIXED — Added `testCheckDependenciesZoltuFactoryCodehashChangedReverts` which etches factory to `hex"00"` (with `makePersistent`) and expects `DependencyChanged` with `keccak256(hex"00")` as actual codehash. + +### P2-A01-3 — `deployToNetworks` -- `DependencyChanged` for Zoltu factory is untested +**Status:** FIXED — Added `testDeployToNetworksZoltuFactoryMissingReverts` (factory etched empty, expects codehash `0xc5d246...`) and `testDeployToNetworksZoltuFactoryCodehashChangedReverts` (factory etched to `hex"00"`, expects `keccak256(hex"00")`). + +### P2-A01-4 — `deployZoltu` -- `DeployFailed` with `success=false` is untested +**Status:** FIXED — Added `MockReverter` contract with reverting constructor and `testDeployZoltuRevertsRevertingConstructor` which expects `DeployFailed(false, address(0))`. + +### P2-A01-5 — `deployToNetworks` -- skip-deployment path is untested +**Status:** FIXED — Added `testDeployToNetworksSkipsWhenAlreadyDeployed` which deploys `MockDeployable` first (with `makePersistent`), then calls `deployToNetworks` again verifying the skip path returns the correct address with matching codehash. + +### P3-A01-1 — ZOLTU_FACTORY NatSpec calls it a "proxy" instead of "factory" +**Status:** FIXED — Changed "Zoltu proxy" to "Zoltu factory" in NatSpec comment. + +### P3-A01-2 — DependencyChanged NatSpec is narrower than actual usage +**Status:** FIXED — Changed NatSpec to "Thrown when a dependency's code hash does not match the expected value." + +### P4-A01-1 — `(forkId);` no-op pattern to suppress unused variable warning +**Status:** DOCUMENTED — Added inline comment explaining the pattern is to suppress slither unused-return warning. + +### P4-A01-2 — Inconsistent error handling between `checkDependencies` and `deployToNetworks` +**Status:** FIXED — Split combined `||` checks in `deployToNetworks` into two-step pattern matching `checkDependencies`: first check `code.length == 0` → `MissingDependency`, then check codehash → `DependencyChanged`. Updated tests `testDeployToNetworksZoltuFactoryMissingReverts` and `testDependencyMissingAtDeployTimeReverts` to expect `MissingDependency`. + +### P4-A01-3 — Magic numbers `12` and `20` in assembly block lack inline documentation +**Status:** FIXED — Added inline comments explaining the memory layout: 20 is the address size, 12 = 32 - 20 is the padding offset to right-align the address in the 32-byte scratch space word. diff --git a/src/lib/LibRainDeploy.sol b/src/lib/LibRainDeploy.sol index 377f3f3..d65b3df 100644 --- a/src/lib/LibRainDeploy.sol +++ b/src/lib/LibRainDeploy.sol @@ -26,14 +26,13 @@ library LibRainDeploy { /// Thrown when the deployed code hash does not match the expected code hash. error UnexpectedDeployedCodeHash(bytes32 expected, bytes32 actual); - /// Thrown when a dependency's code hash or size changed between the - /// dependency check and the deployment. + /// Thrown when a dependency's code hash does not match the expected value. error DependencyChanged(string network, address dependency, bytes32 expectedCodeHash, bytes32 actualCodeHash); /// Thrown when no networks are provided for deployment. error NoNetworks(); - /// Zoltu proxy is the same on every network. + /// Zoltu factory is the same on every network. address constant ZOLTU_FACTORY = 0x7A0D94F55792C434d74a40883C6ed8545E406D12; /// Expected codehash of the Zoltu factory contract. @@ -69,7 +68,11 @@ library LibRainDeploy { address zoltuFactory = ZOLTU_FACTORY; bool success; assembly ("memory-safe") { + // Zero scratch space so mload(0) reads a clean 32-byte word. mstore(0, 0) + // The Zoltu factory returns a raw 20-byte address (not ABI-encoded). + // Writing 20 bytes at offset 12 (= 32 - 20) right-aligns the address + // in scratch space so that mload(0) produces a correctly padded value. success := call(gas(), zoltuFactory, 0, add(creationCode, 0x20), mload(creationCode), 12, 20) deployedAddress := mload(0) } @@ -105,7 +108,11 @@ library LibRainDeploy { address[] memory dependencies, mapping(string => mapping(address => bytes32)) storage depCodeHashes ) internal { + if (networks.length == 0) { + revert NoNetworks(); + } for (uint256 i = 0; i < networks.length; i++) { + // Capture return value to suppress slither unused-return warning. uint256 forkId = vm.createSelectFork(networks[i]); (forkId); console2.log("Block number:", block.number); @@ -153,23 +160,31 @@ library LibRainDeploy { address[] memory dependencies, mapping(string => mapping(address => bytes32)) storage depCodeHashes ) internal returns (address deployedAddress) { + if (networks.length == 0) { + revert NoNetworks(); + } for (uint256 i = 0; i < networks.length; i++) { console2.log("Deploying to network:", networks[i]); + // Capture return value to suppress slither unused-return warning. uint256 forkId = vm.createSelectFork(networks[i]); (forkId); console2.log("Block number:", block.number); + // Re-verify Zoltu factory exists. + if (ZOLTU_FACTORY.code.length == 0) { + revert MissingDependency(networks[i], ZOLTU_FACTORY); + } // Re-verify Zoltu factory codehash. - if (ZOLTU_FACTORY.code.length == 0 || ZOLTU_FACTORY.codehash != ZOLTU_FACTORY_CODEHASH) { + if (ZOLTU_FACTORY.codehash != ZOLTU_FACTORY_CODEHASH) { revert DependencyChanged(networks[i], ZOLTU_FACTORY, ZOLTU_FACTORY_CODEHASH, ZOLTU_FACTORY.codehash); } // Re-verify dependencies have not changed since the check phase. for (uint256 j = 0; j < dependencies.length; j++) { - if ( - dependencies[j].code.length == 0 - || dependencies[j].codehash != depCodeHashes[networks[i]][dependencies[j]] - ) { + if (dependencies[j].code.length == 0) { + revert MissingDependency(networks[i], dependencies[j]); + } + if (dependencies[j].codehash != depCodeHashes[networks[i]][dependencies[j]]) { revert DependencyChanged( networks[i], dependencies[j], diff --git a/test/src/lib/LibRainDeploy.t.sol b/test/src/lib/LibRainDeploy.t.sol index 45d32db..10293cb 100644 --- a/test/src/lib/LibRainDeploy.t.sol +++ b/test/src/lib/LibRainDeploy.t.sol @@ -12,6 +12,15 @@ contract MockDeployable { uint256 public value = 42; } +/// @title MockReverter +/// Contract whose constructor always reverts, used to test DeployFailed with +/// success=false. +contract MockReverter { + constructor() { + revert(); + } +} + /// @title LibRainDeployTest /// Tests for `LibRainDeploy`. External wrappers are used for library functions /// that need `vm.expectRevert` at the correct call depth, and for functions @@ -93,6 +102,24 @@ contract LibRainDeployTest is Test { this.externalDeployAndBroadcast(networks, 1, hex"", "", address(0), bytes32(0), dependencies); } + /// `checkDependencies` MUST revert with `NoNetworks` when given an empty + /// networks array. + function testCheckDependenciesNoNetworksReverts() external { + string[] memory networks = new string[](0); + address[] memory dependencies = new address[](0); + vm.expectRevert(abi.encodeWithSelector(LibRainDeploy.NoNetworks.selector)); + this.externalCheckDependencies(networks, dependencies); + } + + /// `deployToNetworks` MUST revert with `NoNetworks` when given an empty + /// networks array. + function testDeployToNetworksNoNetworksReverts() external { + string[] memory networks = new string[](0); + address[] memory dependencies = new address[](0); + vm.expectRevert(abi.encodeWithSelector(LibRainDeploy.NoNetworks.selector)); + this.externalDeployToNetworks(networks, address(this), hex"", "", address(0), bytes32(0), dependencies); + } + /// External wrapper for `checkDependencies` so that `vm.expectRevert` /// works at the correct call depth. /// @param networks The list of network names to check. @@ -157,6 +184,14 @@ contract LibRainDeployTest is Test { this.externalDeployZoltu(type(MockDeployable).creationCode); } + /// `deployZoltu` MUST revert with `DeployFailed` when the creation code + /// has a reverting constructor (success=false from factory call). + function testDeployZoltuRevertsRevertingConstructor() external { + vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + vm.expectRevert(abi.encodeWithSelector(LibRainDeploy.DeployFailed.selector, false, address(0))); + this.externalDeployZoltu(type(MockReverter).creationCode); + } + /// `deployToNetworks` MUST revert with `UnexpectedDeployedAddress` when the /// deployed address does not match the expected address. function testUnexpectedDeployedAddressReverts() external { @@ -215,6 +250,32 @@ contract LibRainDeployTest is Test { assertEq(deployed, 0xC24016f209562fc151e5Ab7F88694ED5775feb36); } + /// `deployToNetworks` MUST skip deployment and return the expected address + /// when code already exists there, provided the codehash matches. + function testDeployToNetworksSkipsWhenAlreadyDeployed() external { + vm.makePersistent(address(this)); + + vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + address deployed = this.externalDeployZoltu(type(MockDeployable).creationCode); + assertEq(deployed, 0xC24016f209562fc151e5Ab7F88694ED5775feb36); + vm.makePersistent(deployed); + + string[] memory networks = new string[](1); + networks[0] = LibRainDeploy.ARBITRUM_ONE; + address[] memory dependencies = new address[](0); + + address result = this.externalDeployToNetworks( + networks, + address(this), + type(MockDeployable).creationCode, + "test/src/lib/LibRainDeploy.t.sol:MockDeployable", + 0xC24016f209562fc151e5Ab7F88694ED5775feb36, + 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483, + dependencies + ); + assertEq(result, 0xC24016f209562fc151e5Ab7F88694ED5775feb36); + } + /// `checkDependencies` MUST record the codehash of each dependency in the /// storage mapping after verifying it exists. function testCheckDependenciesRecordsCodehash() external { @@ -228,6 +289,48 @@ contract LibRainDeployTest is Test { assertTrue(sDepCodeHashes[LibRainDeploy.ARBITRUM_ONE][LibRainDeploy.ZOLTU_FACTORY] != bytes32(0)); } + /// `checkDependencies` MUST revert with `MissingDependency` when the + /// Zoltu factory has no code on the network. + function testCheckDependenciesMissingZoltuFactoryReverts() external { + vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + vm.makePersistent(LibRainDeploy.ZOLTU_FACTORY); + vm.etch(LibRainDeploy.ZOLTU_FACTORY, hex""); + + string[] memory networks = new string[](1); + networks[0] = LibRainDeploy.ARBITRUM_ONE; + address[] memory dependencies = new address[](0); + + vm.expectRevert( + abi.encodeWithSelector( + LibRainDeploy.MissingDependency.selector, LibRainDeploy.ARBITRUM_ONE, LibRainDeploy.ZOLTU_FACTORY + ) + ); + this.externalCheckDependencies(networks, dependencies); + } + + /// `checkDependencies` MUST revert with `DependencyChanged` when the + /// Zoltu factory exists but has a wrong codehash. + function testCheckDependenciesZoltuFactoryCodehashChangedReverts() external { + vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + vm.makePersistent(LibRainDeploy.ZOLTU_FACTORY); + vm.etch(LibRainDeploy.ZOLTU_FACTORY, hex"00"); + + string[] memory networks = new string[](1); + networks[0] = LibRainDeploy.ARBITRUM_ONE; + address[] memory dependencies = new address[](0); + + vm.expectRevert( + abi.encodeWithSelector( + LibRainDeploy.DependencyChanged.selector, + LibRainDeploy.ARBITRUM_ONE, + LibRainDeploy.ZOLTU_FACTORY, + LibRainDeploy.ZOLTU_FACTORY_CODEHASH, + keccak256(hex"00") + ) + ); + this.externalCheckDependencies(networks, dependencies); + } + /// `checkDependencies` MUST revert with `MissingDependency` when a /// dependency has no code on the network. function testMissingDependencyReverts() external { @@ -244,6 +347,54 @@ contract LibRainDeployTest is Test { this.externalCheckDependencies(networks, dependencies); } + /// `deployToNetworks` MUST revert with `MissingDependency` when the + /// Zoltu factory has been removed since the check phase. + function testDeployToNetworksZoltuFactoryMissingReverts() external { + vm.makePersistent(address(this)); + + vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + vm.makePersistent(LibRainDeploy.ZOLTU_FACTORY); + vm.etch(LibRainDeploy.ZOLTU_FACTORY, hex""); + + string[] memory networks = new string[](1); + networks[0] = LibRainDeploy.ARBITRUM_ONE; + address[] memory dependencies = new address[](0); + + vm.expectRevert( + abi.encodeWithSelector( + LibRainDeploy.MissingDependency.selector, + LibRainDeploy.ARBITRUM_ONE, + LibRainDeploy.ZOLTU_FACTORY + ) + ); + this.externalDeployToNetworks(networks, address(this), hex"", "", address(0), bytes32(0), dependencies); + } + + /// `deployToNetworks` MUST revert with `DependencyChanged` when the + /// Zoltu factory has a wrong codehash at deploy time. + function testDeployToNetworksZoltuFactoryCodehashChangedReverts() external { + vm.makePersistent(address(this)); + + vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + vm.makePersistent(LibRainDeploy.ZOLTU_FACTORY); + vm.etch(LibRainDeploy.ZOLTU_FACTORY, hex"00"); + + string[] memory networks = new string[](1); + networks[0] = LibRainDeploy.ARBITRUM_ONE; + address[] memory dependencies = new address[](0); + + vm.expectRevert( + abi.encodeWithSelector( + LibRainDeploy.DependencyChanged.selector, + LibRainDeploy.ARBITRUM_ONE, + LibRainDeploy.ZOLTU_FACTORY, + LibRainDeploy.ZOLTU_FACTORY_CODEHASH, + keccak256(hex"00") + ) + ); + this.externalDeployToNetworks(networks, address(this), hex"", "", address(0), bytes32(0), dependencies); + } + /// `deployToNetworks` MUST revert with `DependencyChanged` when a /// dependency's codehash differs from what was recorded during the check /// phase. @@ -272,9 +423,9 @@ contract LibRainDeployTest is Test { this.externalDeployToNetworks(networks, address(this), hex"", "", address(0), bytes32(0), dependencies); } - /// `deployToNetworks` MUST revert with `DependencyChanged` when a + /// `deployToNetworks` MUST revert with `MissingDependency` when a /// dependency has been destroyed (code.length == 0) since the check phase. - function testDependencyChangedCodeLengthReverts() external { + function testDependencyMissingAtDeployTimeReverts() external { // Make the test contract persistent so storage survives fork switches. vm.makePersistent(address(this)); @@ -288,11 +439,9 @@ contract LibRainDeployTest is Test { vm.expectRevert( abi.encodeWithSelector( - LibRainDeploy.DependencyChanged.selector, + LibRainDeploy.MissingDependency.selector, LibRainDeploy.ARBITRUM_ONE, - address(0xdead), - bytes32(uint256(1)), - 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 + address(0xdead) ) ); this.externalDeployToNetworks(networks, address(this), hex"", "", address(0), bytes32(0), dependencies); From 4927c0527bbd28837eaeaf341dc3b024da3ccd3e Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Thu, 5 Mar 2026 21:42:36 +0400 Subject: [PATCH 9/9] address coderabbit review feedback on audit commit - Expand checkDependencies NatSpec to mention codehash verification - Document idempotent skip behavior in deployToNetworks NatSpec - Add bash language identifier to CLAUDE.md RPC fence block - Fix absolute paths in pass3/pass4 audit files - Fix MD058 (blank lines around tables) in audit markdown files - Fix MD022 (blank line after heading) in pass0 Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 2 +- audit/2026-03-05-02/pass0/process.md | 1 + audit/2026-03-05-02/pass1/LibRainDeploy.md | 3 +++ audit/2026-03-05-02/pass2/LibRainDeploy.md | 7 +++++++ audit/2026-03-05-02/pass3/LibRainDeploy.md | 2 +- audit/2026-03-05-02/pass4/LibRainDeploy.md | 2 +- audit/2026-03-05-02/pass5/LibRainDeploy.md | 4 ++++ src/lib/LibRainDeploy.sol | 8 +++++--- 8 files changed, 23 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8bd1bd4..9fbab8a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,7 +38,7 @@ CI runs three matrix tasks: `rainix-sol-legal`, `rainix-sol-test`, `rainix-sol-s ## RPC Configuration Fork tests require RPC endpoints defined in `.env` (gitignored): -``` +```bash ARBITRUM_RPC_URL=https://arb1.arbitrum.io/rpc BASE_RPC_URL=https://mainnet.base.org FLARE_RPC_URL=https://flare-api.flare.network/ext/C/rpc diff --git a/audit/2026-03-05-02/pass0/process.md b/audit/2026-03-05-02/pass0/process.md index cda87aa..ce04f7d 100644 --- a/audit/2026-03-05-02/pass0/process.md +++ b/audit/2026-03-05-02/pass0/process.md @@ -1,6 +1,7 @@ # Pass 0: Process Review — 2026-03-05-02 ## Documents Reviewed + - `CLAUDE.md` - `foundry.toml` - `slither.config.json` diff --git a/audit/2026-03-05-02/pass1/LibRainDeploy.md b/audit/2026-03-05-02/pass1/LibRainDeploy.md index a4885bb..da7eef6 100644 --- a/audit/2026-03-05-02/pass1/LibRainDeploy.md +++ b/audit/2026-03-05-02/pass1/LibRainDeploy.md @@ -8,6 +8,7 @@ **Library name:** `LibRainDeploy` (line 14) **Functions:** + | Function | Line | Visibility | |----------|------|------------| | `etchZoltuFactory(Vm vm)` | 60 | internal | @@ -18,6 +19,7 @@ | `deployAndBroadcast(Vm, string[], uint256, bytes, string, address, bytes32, address[], mapping(...))` | 222 | internal | **Errors:** + | Error | Line | |-------|------| | `DeployFailed(bool success, address deployedAddress)` | 18 | @@ -28,6 +30,7 @@ | `NoNetworks()` | 34 | **Constants:** + | Constant | Line | Type | |----------|------|------| | `ZOLTU_FACTORY` | 37 | `address` | diff --git a/audit/2026-03-05-02/pass2/LibRainDeploy.md b/audit/2026-03-05-02/pass2/LibRainDeploy.md index 5be4193..e23aa64 100644 --- a/audit/2026-03-05-02/pass2/LibRainDeploy.md +++ b/audit/2026-03-05-02/pass2/LibRainDeploy.md @@ -9,6 +9,7 @@ ### Source: `LibRainDeploy` (library, line 14) **Functions:** + | Function | Line | Visibility | |----------|------|------------| | `etchZoltuFactory(Vm vm)` | 60 | internal | @@ -19,6 +20,7 @@ | `deployAndBroadcast(Vm, string[], uint256, bytes, string, address, bytes32, address[], mapping(...))` | 222 | internal | **Errors:** + | Error | Line | |-------|------| | `DeployFailed(bool success, address deployedAddress)` | 18 | @@ -29,6 +31,7 @@ | `NoNetworks()` | 34 | **Constants:** + | Constant | Line | Type | |----------|------|------| | `ZOLTU_FACTORY` | 37 | `address` | @@ -44,6 +47,7 @@ ### Test: `LibRainDeployTest` (contract, line 19) with `MockDeployable` (line 10) **Tests:** + | Test | Line | |------|------| | `testSupportedNetworks()` | 24 | @@ -62,6 +66,7 @@ | `testDependencyChangedCodeLengthReverts()` | 277 | **External wrappers:** + | Wrapper | Line | |---------|------| | `externalDeployAndBroadcast(...)` | 66 | @@ -70,11 +75,13 @@ | `externalDeployZoltu(...)` | 139 | **Mock contracts:** + | Contract | Line | Purpose | |----------|------|---------| | `MockDeployable` | 10 | Minimal contract with `value = 42` for Zoltu deployment tests | **Storage:** + | Variable | Line | Type | |----------|------|------| | `sDepCodeHashes` | 20 | `mapping(string => mapping(address => bytes32))` | diff --git a/audit/2026-03-05-02/pass3/LibRainDeploy.md b/audit/2026-03-05-02/pass3/LibRainDeploy.md index fe07a75..91fe89a 100644 --- a/audit/2026-03-05-02/pass3/LibRainDeploy.md +++ b/audit/2026-03-05-02/pass3/LibRainDeploy.md @@ -1,7 +1,7 @@ # Pass 3 (Documentation) - LibRainDeploy.sol **Agent:** A01 -**File:** `/Users/thedavidmeister/Code/rain.deploy/src/lib/LibRainDeploy.sol` +**File:** `src/lib/LibRainDeploy.sol` ## Evidence of Thorough Reading diff --git a/audit/2026-03-05-02/pass4/LibRainDeploy.md b/audit/2026-03-05-02/pass4/LibRainDeploy.md index 6d16709..7541314 100644 --- a/audit/2026-03-05-02/pass4/LibRainDeploy.md +++ b/audit/2026-03-05-02/pass4/LibRainDeploy.md @@ -1,7 +1,7 @@ # Pass 4 (Code Quality) - LibRainDeploy.sol **Agent:** A01 -**File:** `/Users/thedavidmeister/Code/rain.deploy/src/lib/LibRainDeploy.sol` (253 lines) +**File:** `src/lib/LibRainDeploy.sol` (253 lines) ## Evidence of Thorough Reading diff --git a/audit/2026-03-05-02/pass5/LibRainDeploy.md b/audit/2026-03-05-02/pass5/LibRainDeploy.md index 27f7b41..42f8460 100644 --- a/audit/2026-03-05-02/pass5/LibRainDeploy.md +++ b/audit/2026-03-05-02/pass5/LibRainDeploy.md @@ -14,6 +14,7 @@ **Library:** `LibRainDeploy` (line 14) **Functions:** + | Function | Line | |---|---| | `etchZoltuFactory(Vm)` | 60 | @@ -24,6 +25,7 @@ | `deployAndBroadcast(Vm, string[], uint256, bytes, string, address, bytes32, address[], mapping)` | 222 | **Errors:** + | Error | Line | |---|---| | `DeployFailed(bool success, address deployedAddress)` | 18 | @@ -34,6 +36,7 @@ | `NoNetworks()` | 34 | **Constants:** + | Constant | Line | Value | |---|---|---| | `ZOLTU_FACTORY` | 37 | `0x7A0D94F55792C434d74a40883C6ed8545E406D12` | @@ -54,6 +57,7 @@ - `sDepCodeHashes` (line 20) - `mapping(string => mapping(address => bytes32))` **Functions (test contract):** + | Function | Line | |---|---| | `testSupportedNetworks()` | 24 | diff --git a/src/lib/LibRainDeploy.sol b/src/lib/LibRainDeploy.sol index d65b3df..2401792 100644 --- a/src/lib/LibRainDeploy.sol +++ b/src/lib/LibRainDeploy.sol @@ -96,8 +96,9 @@ library LibRainDeploy { return networks; } - /// Checks that the Zoltu factory and all dependencies have code on each - /// network. Records each dependency's codehash in the provided mapping. + /// Checks that the Zoltu factory has the expected codehash and all + /// dependencies have code on each network. Records each dependency's + /// codehash in the provided mapping. /// @param vm The Vm instance to use for forking. /// @param networks The list of network names to check. /// @param dependencies The addresses that must have code on each network. @@ -138,7 +139,8 @@ library LibRainDeploy { } /// Verifies that dependencies have not changed since the check phase, - /// then deploys to each network via the Zoltu factory. + /// then deploys to each network via the Zoltu factory. If code already + /// exists at `expectedAddress`, deployment is skipped for that network. /// @param vm The Vm instance to use for forking and broadcasting. /// @param networks The list of network names to deploy to. /// @param deployer The deployer address.