Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9a0cc8d
feat(registry): address registry interface, reader lib and cross-netw…
thedavidmeister Aug 8, 2026
0958f42
test(registry): fork two networks in the cross-network gate test, not…
thedavidmeister Aug 8, 2026
bf5ac30
docs: Zoltu deploys with CREATE2 under a zero salt, not CREATE with a…
thedavidmeister Aug 8, 2026
605c1c2
feat(registry): mutable bindings, post-deploy verification, concrete …
thedavidmeister Aug 8, 2026
d58d007
test(deploy): repin testDeployZoltu's literal to this repo's compiler…
thedavidmeister Aug 8, 2026
10b14d5
feat(verify): one inherited deploy-pin verification, parameterized ov…
thedavidmeister Aug 13, 2026
4662b77
fix(verify): one contract per file, and keep slither on deployable co…
thedavidmeister Aug 13, 2026
226befc
feat(deploy): the on-chain deploy this repo had no way to run
thedavidmeister Aug 13, 2026
7461a93
fix(review): close the CodeRabbit findings that were real
thedavidmeister Aug 13, 2026
58a2cc8
feat(suites): one declaration, deployed and verified, as a registry
thedavidmeister Aug 13, 2026
7c60080
refactor(build): use the codegen library for codegen, don't reimpleme…
thedavidmeister Aug 13, 2026
589686c
feat(snapshot): candidate/frozen split, LibSnapshot moved in, .pointe…
thedavidmeister Aug 13, 2026
a4e5a7b
test(exemplars): the exemplar owns the shape, the compiler owns the v…
thedavidmeister Aug 13, 2026
721186f
test(snapshots): generate the test records, specify their shape from …
thedavidmeister Aug 13, 2026
e7b0384
refactor(snapshot): share the alias-lib emitter, and stop swallowing …
thedavidmeister Aug 13, 2026
f7e0fe5
test(snapshot): prove the release guards fire
thedavidmeister Aug 13, 2026
f04e559
refactor(verify): name the subject, and drive the tests from the real…
thedavidmeister Aug 13, 2026
befb78d
refactor(snapshot): bump rain-sol-codegen to 0.1.6 and use its filePr…
thedavidmeister Aug 13, 2026
a94040f
feat(verify): zero root for rollout, and chain checks only what released
thedavidmeister Aug 13, 2026
d8ebf90
fix(ci): slither filter follows the rename, SPDX fixture stops declar…
thedavidmeister Aug 13, 2026
f6c87dc
style: forge fmt, and the lint directive sits against the line it sup…
thedavidmeister Aug 13, 2026
0c5967d
fix(snapshot): a failed freeze is retryable, and the writer has one root
thedavidmeister Aug 13, 2026
0b7db36
fix(verify): the record is matched on what it declares, not on its text
thedavidmeister Aug 13, 2026
e37f499
test(chain): the candidate scope gets its own file
thedavidmeister Aug 13, 2026
95a41bf
feat(release): the record and the declaration of it come from one call
thedavidmeister Aug 14, 2026
dd2d93c
fix(release): a released lib describes one contract, not the record w…
thedavidmeister Aug 14, 2026
eb0b3f5
fix(verify): read the declaration the compiler reads, and restore the…
thedavidmeister Aug 14, 2026
696f2e5
style(test): wrap the writer call forge fmt wraps
thedavidmeister Aug 14, 2026
57618df
Review fixes: slither scope, address decode, and doc claims that were…
thedavidmeister Aug 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .coderabbitai.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# SPDX-License-Identifier: LicenseRef-DCL-1.0
# SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd

reviews:
path_filters:
- "!audit/**"
Expand Down
23 changes: 23 additions & 0 deletions .github/workflows/manual-sol-artifacts.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Manual sol artifacts
# The on-chain deploy, run by hand. This repo carries a deployed concrete
# (`AddressRegistry`) whose address + codehash consumers pin, and
# `package-release.yaml` cuts a release for a deployment that ALREADY exists —
# rainix-tag-release verifies the live chains against freshly generated pins and
# never broadcasts. So the deploy has to happen first, and separately, which is
# this.
#
# Order is: dispatch this, confirm `AddressRegistryDeployPinsChainTest` passes
# on every supported network, then push the `sol-v*` tag.
#
# Deliberately `workflow_dispatch` only. Broadcasting is key custody and real
# money; nothing about a merge or a tag should trigger it.
on:
workflow_dispatch:
jobs:
deploy:
uses: rainlanguage/rainix/.github/workflows/rainix-manual-sol-artifacts.yaml@main
with:
# Passed through as DEPLOYMENT_SUITE; script/Deploy.sol dispatches on it
# and reverts on anything else.
suite: address-registry
secrets: inherit
22 changes: 19 additions & 3 deletions .github/workflows/package-release.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
name: Package Release
# Deploy repo: a manual `sol-v*` tag is the sole release trigger. This repo now
# carries a deployed concrete (`AddressRegistry`) whose address + codehash
# consumers pin, which is exactly the shape rainix-tag-release exists for and
# exactly the shape rainix-autopublish's merge-driven, next-version lifecycle is
# wrong for: autopublish bumps [package].version on every merge while the frozen
# deploy tag only advances at deploy time.
#
# The tag names the version; rainix-tag-release regenerates the snapshot for it,
# verifies the live chains match the fresh pins, publishes rain-deploy to
# Soldeer, and commits the frozen snapshot back to main. The on-chain deploy is
# separate and manual, run before tagging; this never broadcasts.
#
# Switching lifecycles retracts nothing: every version already published stays
# published, and consumers pin exact versions, so this changes who cuts a
# release and nothing about how anyone consumes one.
on:
push:
branches:
- main
tags:
- sol-v*
jobs:
release:
uses: rainlanguage/rainix/.github/workflows/rainix-autopublish.yaml@main
uses: rainlanguage/rainix/.github/workflows/rainix-tag-release.yaml@main
with:
soldeer-package: rain-deploy
snapshot-generate-cmd: forge script ./script/Build.sol && forge fmt
secrets: inherit
Comment thread
coderabbitai[bot] marked this conversation as resolved.
226 changes: 207 additions & 19 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@

# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
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) because the Zoltu proxy is deployed at the same address on every chain and uses CREATE with a predictable nonce.
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, Base
Sepolia, Flare, Polygon) because the Zoltu proxy is deployed at the same address
on every chain and deploys with `CREATE2` over its calldata under a zero salt,
so a contract's address is a pure function of its creation code.

## Build & Development

This project uses **Foundry** (forge) for Solidity development and **Nix** for environment management.
This project uses **Foundry** (forge) for Solidity development and **Nix** for
environment management.

```bash
# Enter the nix dev shell (provides forge and all tooling)
Expand All @@ -33,40 +40,221 @@ nix develop -c rainix-sol-static
nix develop -c rainix-sol-legal
```

CI runs three matrix tasks: `rainix-sol-legal`, `rainix-sol-test`, `rainix-sol-static`.
CI runs three matrix tasks: `rainix-sol-legal`, `rainix-sol-test`,
`rainix-sol-static`. There is a fourth workflow, `Manual sol artifacts`, which
is `workflow_dispatch` only and is the on-chain deploy — nothing automatic ever
broadcasts.

## 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
BASE_SEPOLIA_RPC_URL=https://sepolia.base.org
FLARE_RPC_URL=https://flare-api.flare.network/ext/C/rpc
POLYGON_RPC_URL=https://polygon-rpc.com
POLYGON_RPC_URL=https://polygon-bor-rpc.publicnode.com
```

All five are needed: `RainDeployVerifyChain` forks every network in
`supportedNetworks()`, so a missing or rate-limited endpoint fails it. Those
failures are `vm.createSelectFork` errors, distinct from the
`NotDeployedOnNetwork` a reachable network raises, and the offline contracts run
regardless: `forge test --no-match-contract Chain`.

These are referenced in `foundry.toml` under `[rpc_endpoints]`.

## Architecture

The entire library is a single file: `src/lib/LibRainDeploy.sol`.
**`src/lib/LibRainDeploy.sol`** — the deploy library:

- `etchZoltuFactory(Vm)` — etches the Zoltu factory bytecode at the factory
address (for networks where it isn't deployed)
- `zoltuAddress(bytes creationCode)` — derives the address the factory deploys
creation code to, without deploying
- `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)
- `isStartBlock(...)` / `findDeployBlock(...)` — binary search a fork's history
for the block a contract first appears at
- `checkResolvedAddresses(...)` — asserts an already-deployed contract holds the
addresses the deployment expected, on the currently selected fork, via
consumer-supplied static reads
- `checkResolvedAddressesOnNetworks(...)` — runs that check on every network. It
runs AFTER the deploy, against state the deployment has already settled, which
is the only point at which such a check means anything: registry bindings are
mutable, so a pre-deploy check would read a source that can change before the
constructor that consumes it
- `deployToNetworks(...)` — forks each network, verifies the factory and
dependencies, deploys via Zoltu, verifies address and code hash
- `deployAndBroadcast(...)` — the main entry point: derives the deployer from a
private key, then `deployToNetworks`

**`src/interface/IAddressRegistryV1.sol`** — the address registry interface: an
immutable root binds a `bytes32` name to an address (`register`), anyone reads a
bound name (`get`), and reading an unbound name reverts. Bindings are mutable so
an owning multisig can rotate without moving any consumer's deterministic
address; a consumer resolves once in its constructor and stores the answer, so a
re-binding never moves anything already deployed.

**`src/concrete/AddressRegistry.sol`** — the implementation. Two functions and
nothing else. `ADDRESS_REGISTRY_ROOT` is a compile-time constant and therefore
part of the creation code, so changing it moves the deterministic address and
code hash.

**`src/lib/LibAddressRegistryDeploy.sol`** — those pins, derived from the
creation code this repo compiles under this repo's own settings and checked
against it by `AddressRegistryDeployPinsOfflineTest`. Hand-written until the
first `sol-v*` release generates it from `src/generated/<tag>/`; no snapshot is
frozen while the root is a placeholder, because that directory is append-only.

**`src/lib/LibAddressRegistry.sol`** — reads that registry at its deterministic
address, verifying its code hash first, exactly as `LibRainDeploy` verifies
`ZOLTU_FACTORY_CODEHASH`. It resolves a name to an address and nothing more:
what a consumer resolves a name for, and when, is the consumer's business.

### `src/` holds the deploy machinery here. That is a SCOPED EXCEPTION.

`src/abstract/RainDeploy*.sol` are test and script infrastructure, and they live
in `src/` rather than `test/`. Two reasons, and the second is the one that
matters:

1. `.soldeerignore` excludes `test/` from the published package, and a
downstream repo has to import all of this — its `script/Deploy.sol` inherits
`RainDeployBroadcast`, its test contracts inherit `RainDeployVerify*`. An
abstract in a path the package excludes is unusable by every consumer.
2. **This repo's PRODUCT is the deployment process.** Machinery for deploying
and for verifying deployments is not scaffolding that happens to live here —
it is the thing the package exists to publish. So `src/` is where it belongs.

**Do not copy this into a consumer repo.** There, `src/` is the product —
tokens, vaults, a factory — and deploy verification is scaffolding around it, so
the usual convention stands unchanged: `test/src/**` mirrors `src/**`, and test
abstracts live under `test/`. The exception is earned by what this repo IS, and
a repo that merely USES this machinery has not earned it.

The exception is scoped to the deploy/verify abstracts and the suite
declaration. `src/concrete/AddressRegistry.sol` is an ordinary deployed
contract, tested from `test/src/concrete/` exactly as the convention requires.

**`src/abstract/RainDeploySuitesBase.sol`** — the ONE declaration of what a repo
deploys: per suite, a key, the creation code, the recorded address/code
hash/runtime code, an artifact path, and dependencies.

Both sides read it. `RainDeployBroadcast` deploys from it and
`RainDeployVerify*` verify against it, so "the deploy script broadcasts one
contract while the tests verify another" is not a statement that can be true —
not because something checks for it, but because there is one array and all
three contracts read it.

Suites are a REGISTRY the abstract iterates, not a chain of `else if`. A repo
adds a suite by adding an array entry; the keys reported by a mistyped
`DEPLOYMENT_SUITE` are built from that same array, so the failure message cannot
fall behind the suites it describes. Keys are checked unique, because the key is
what selects what gets broadcast.

**`src/abstract/RainDeployBroadcast.sol`** — the broadcast. Selects one suite by
`DEPLOYMENT_SUITE` and deploys it, before reading `DEPLOYMENT_KEY` so a mistyped
suite fails naming the valid ones rather than on a missing key.
`deployNetworks()` defaults to `supportedNetworks()` and is overridable for
repos that bootstrap one chain per dispatch.

**`script/Deploy.sol`** —
`contract Deploy is AddressRegistryDeploySuites,
RainDeployBroadcast {}`. Empty
on purpose: the suites and the broadcast are both inherited. Run only via the
`Manual sol artifacts` workflow.

**`src/abstract/AddressRegistryDeploySuites.sol`** — this repo's own
declaration, inherited by `script/Deploy.sol` and by both pins test contracts.

**`src/abstract/RainDeployVerify*.sol`** — the deploy-pin verification every
deploy repo inherits instead of hand-writing.

Nothing is per suite beyond an array entry, and nothing anywhere is per network.

The creation code is the only parameter. The Zoltu factory is `CREATE2` over its
calldata under a zero salt, so the address is a pure function of it, and running
it once locally gives the runtime code and its hash. The address, code hash and
runtime code a pointers file records are checked OUTPUTS.

Three groups, sorted by what they are anchored to:

1. **Internal to the recorded set** (`RainDeployVerifyOffline`) — what a version
records is what its own creation code derives. Catches a set generated
inconsistently. CANNOT catch a snapshot of the wrong contract: a consistent
snapshot of the wrong thing satisfies all of it, which
`testWrongContractSnapshotPassesInternalConsistency` pins.
2. **Anchored to source** (`RainDeployVerifyOffline`) — the candidate's recorded
creation code is `type(X).creationCode`. The only check that catches a
wrong-contract snapshot. Candidate only, because a released tag is MEANT to
diverge from current source; there is no field on a released version to spell
it, so it cannot be opted into or out of.
3. **Anchored to chain** (`RainDeployVerifyChain`) — across
`supportedNetworks()`, every version's derived address carries code with its
derived code hash. The only check that catches "never deployed" or "not there
any more", neither of which the repo can hold: both go false with nobody
touching it.

Group 3 lives in its own contract so an unreachable RPC endpoint fails only it,
never the assertions that hold offline — `forge test --no-match-contract Chain`
is the whole offline gate, and nothing reachable from those contracts forks
anything.

A single recorded code hash per version can only be true if the runtime code is
the same on every network, so a constructor reading `block.chainid` or similar
is a DEFECT: it fails hard, naming the chain and both hashes. There is
deliberately no per-chain code hash to record.

**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)
- `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 `src/abstract/` files are the only `src/` files `slither.config.json`
filters out, by name. They are inherited by test contracts and never deployed,
so slither's detectors — all of which are about deployed-code risk — have
nothing to say about them except that an abstract does not implement its own
virtuals and that a cheatcode is called in a loop, and — because slither skips
`test/` and `script/` — that an abstract's virtuals have no caller. The filter
matches those filenames exactly, not the `src/abstract/` prefix, so a file added
there later — including a deployable one — is analyzed rather than silently
exempted.

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.
The libraries are 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.
- **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`. The address registry is verified the same way before it is
read.
- **Dependency checking**: Before deploying to any network, all dependencies
(contract addresses) are verified to have code on-chain.
- **Idempotent deploys**: If code already exists at the expected address,
deployment is skipped for that network.
- **Resolve once, verify after**: registry bindings are mutable, so the
meaningful check is not "does the registry say what I expect" before a deploy
but "does the deployed contract hold what I expect" after one. A consumer
resolves in its constructor; the deployment is then verified across every
network before anything migrates onto it.
- **Deploy-repo lifecycle**: a manual `sol-v*` tag is the sole release trigger
(`rainix-tag-release`), because this repo carries a deployed concrete whose
pins consumers rely on. `[package].version` is the LAST released version and
moves only in lockstep with its snapshot.
- **Deploy, then verify, then tag** — in that order, and they are three separate
things. `script/Deploy.sol` broadcasts `AddressRegistry` to every network in
`supportedNetworks()`, dispatched by hand through
`.github/workflows/manual-sol-artifacts.yaml`. Only then can
`AddressRegistryDeployPinsChainTest` pass, and only then is there a deployment
for `rainix-tag-release` to verify pins against — it verifies and publishes,
it never broadcasts. Broadcasting is key custody and real money, so it is
`workflow_dispatch` and nothing else. Deploying is idempotent: a network that
already has the code is skipped, so a partial run is fixed by running it
again.

## License

DecentraLicense 1.0 (LicenseRef-DCL-1.0). All source files must have SPDX headers. REUSE compliance is enforced in CI.
DecentraLicense 1.0 (LicenseRef-DCL-1.0). All source files must have SPDX
headers. REUSE compliance is enforced in CI.
Loading
Loading