Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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/BuildPointers.sol && forge fmt
secrets: inherit
106 changes: 86 additions & 20 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,99 @@ 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`.

## 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
POLYGON_RPC_URL=https://polygon-rpc.com
```

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)
- `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.
**`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 `AddressRegistryDeployPinsTest`. 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.

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.

## 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.
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ It answers:
- Have I deployed successfully to all expected networks?
- How do I track deployments over time and share addresses with other people?
- How do I ensure deployed code is bytecode-equivalent to local compilations?
- How does a deployment get a configured address — an owner, say — without
baking one into its creation code, where changing it would move every future
deployment?

Approach:

Expand All @@ -25,6 +28,55 @@ Approach:
and against the chain after: silent failures fail loudly.
- Bytecode integrity checks (e.g. via the Rain Extrospection lib) supported
post-deploy.
- An address registry, read at run time rather than compiled into creation code,
and a post-deploy check that every target network's deployment took the
address it was supposed to.

## Address registry

`AddressRegistry` binds an opaque `bytes32` name to an address. An immutable
root authority binds a name, anyone reads a bound name, and reading an unbound
name reverts rather than answering with the zero address. There is no removal,
no upgrade and no authority besides root.

Bindings are **mutable**, because the addresses they name are. Rotating an
owning multisig is ordinary business and has to be expressible without moving
anybody's deterministic address — which a binding welded to one address forever
would make impossible, because the name is in the consumer's creation code, so a
new name means new creation code and a new address. That is the problem the
registry exists to remove, not a property worth keeping.

Mutability costs nothing already deployed. A consumer resolves a name **once**,
in its constructor, and stores the answer; it never reads the registry again. So
re-binding a name changes what the _next_ deployment resolves and nothing else,
which makes a rotation a deliberate migration rather than a silent change to
live contracts.

`LibAddressRegistry.resolve` is the read, verifying the registry's code hash
first, the same way `LibRainDeploy` verifies the Zoltu factory's. It resolves a
name to an address and stops there — what a consumer does with the address, and
when, is the consumer's business.

`LibRainDeploy.checkResolvedAddressesOnNetworks` is the **post-deploy**
verification: on every target network, the deployed contract must hold the
address the deployment expected. It runs after the deploy and before anything
depends on it, against state the deployment has already settled, so nothing it
reads can move underneath it. The same check run beforehand would be worth
nothing against a mutable source. A network where the deployment took something
else is a burned deterministic address, found while nothing points at it yet.

Only the consumer knows where it stored what it resolved, so the consumer
supplies the reads (`abi.encodeCall(IOwnable.owner, ())` and the like) and this
library supplies the fork loop and the comparison.

## Releases

This is a deploy repo: it carries a deployed concrete whose address and codehash
consumers pin, so releases are **manual `sol-v*` tags**, not merges.
`[package].version` is the LAST released version, naming the current
`src/generated/<tag>/` snapshot, and only a release moves it. Every version
published under the previous merge-driven lifecycle stays published; consumers
pin exact versions and are unaffected.

## Install

Expand Down
24 changes: 23 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
[package]
name = "rain-deploy"
version = "0.1.6"
# Deploy repo: this is the LAST released version, naming the current
# src/generated/<tag>/ snapshot, not a next-version slot. A normal PR does not
# bump it; only a `sol-v*` tag release moves it, in lockstep with the snapshot.
version = "0.1.5"

# SPDX-License-Identifier: LicenseRef-DCL-1.0
# SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
Expand All @@ -9,11 +12,30 @@ version = "0.1.6"
src = "src"
out = "out"
libs = ["dependencies"]

# This repo compiles a contract whose deterministic deploy address and code hash
# are pinned in LibAddressRegistryDeploy, and both are a pure function of the
# creation code, which is a function of these settings. They are pinned exactly
# rather than floated so the pins cannot move under a compiler or default-target
# change, and they match the settings the org's other deploy repos use.
solc = "0.8.25"
optimizer = true
optimizer_runs = 100000
evm_version = "cancun"
cbor_metadata = false
bytecode_hash = "none"

# BuildPointers reads the version from foundry.toml and writes the generated
# per-tag snapshots + the current-pin lib under src/. Nothing else in this repo
# touches the filesystem.
fs_permissions = [
{ access = "read", path = "./foundry.toml" },
{ access = "read-write", path = "./src" },
]

[dependencies]
forge-std = "1.16.1"
rain-sol-codegen = "0.1.0"

[soldeer]
recursive_deps = false
Expand Down
1 change: 1 addition & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
forge-std-1.16.1/=dependencies/forge-std-1.16.1/
rain-sol-codegen-0.1.0/=dependencies/rain-sol-codegen-0.1.0/
Loading
Loading