Skip to content
Closed
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
4 changes: 4 additions & 0 deletions .gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ CloneFactoryCloneDeterministicTest:testCloneDeterministicMatchesPredict(bytes32,
CloneFactoryCloneDeterministicTest:testCloneDeterministicSaltIsAbiEncodeHash(address,bytes32,address) (runs: 2048, μ: 4686, ~: 4686)
CloneFactoryCloneDeterministicTest:testCloneDeterministicSenderScoped(bytes32,bytes,address,address) (runs: 2048, μ: 585442, ~: 485815)
CloneFactoryCloneDeterministicTest:testCloneDeterministicZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 10699, ~: 10678)
LibCloneFactoryDeployCandidateTest:testCandidateCreationDeploysToPinnedAddress() (gas: 316633)
LibCloneFactoryDeployCandidateTest:testCandidateIsTheAliasedSnapshot() (gas: 3708)
LibCloneFactoryDeployCandidateTest:testCandidateRuntimeHashesToBytecodeHash() (gas: 879)
LibCloneFactoryDeployCandidateTest:testCandidateSelfConsistent() (gas: 4706)
LibCloneFactoryDeployTaggedConstantsTest:testCloneFactory_0_1_3_CreationDeploysToPinnedAddress() (gas: 240145)
LibCloneFactoryDeployTaggedConstantsTest:testCloneFactory_0_1_3_RuntimeHashesToBytecodeHash() (gas: 755)
LibCloneFactoryDeployTaggedConstantsTest:testCloneFactory_0_1_4_CreationDeploysToPinnedAddress() (gas: 240146)
Expand Down
13 changes: 7 additions & 6 deletions .github/workflows/package-release.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
name: Package Release
# Deploy repo: a manual `sol-v*` tag is the sole release trigger. The tag names
# the version; rainix-tag-release verifies prod exists at the regenerated pins,
# publishes rain-factory-deploy to Soldeer, and commits the frozen snapshot back
# to main. The on-chain deploy is separate and manual (rainix-manual-sol-artifacts
# dispatch), run before tagging. Nothing publishes on merge, so [package].version
# and the frozen src/generated/<tag>/ snapshot it names only ever move together.
# the version; rainix-tag-release writes it to foundry.toml, runs cut-release.sh
# to freeze src/generated/candidate/ as src/generated/<tag>/, verifies prod exists
# at the pins, publishes rain-factory-deploy to Soldeer, and commits the frozen
# snapshot back to main. The on-chain deploy is separate and manual
# (rainix-manual-sol-artifacts dispatch), run before tagging. Nothing publishes on
# merge; the pushed tag, not the committed [package].version, decides the release.
on:
push:
tags:
Expand All @@ -14,5 +15,5 @@ jobs:
uses: rainlanguage/rainix/.github/workflows/rainix-tag-release.yaml@main
with:
soldeer-package: rain-factory-deploy
snapshot-generate-cmd: forge script ./script/BuildPointers.sol && forge fmt
snapshot-generate-cmd: bash script/cut-release.sh
secrets: inherit
57 changes: 39 additions & 18 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ nix develop .#sol-shell -c slither .
# License/legal checks (REUSE compliance)
nix develop .#sol-shell -c reuse lint

# Regenerate the deploy pins for the current [package].version
# Regenerate the rolling candidate snapshot + the pin lib. Run this after ANY
# change to CloneFactory or the compiler config, or testCandidateSelfConsistent
# fails. A clean tree must stay clean after running it.
nix develop .#sol-shell -c bash -c 'forge script ./script/BuildPointers.sol && forge fmt'
```

Expand All @@ -69,13 +71,20 @@ as the `rain-factory` Soldeer dependency, so they are read under
`ICloneableFactoryV3`. Uses OpenZeppelin `Clones.cloneDeterministic()`; there
is no plain `clone()`.
- `src/lib/LibCloneFactoryDeploy.sol` — Deterministic deployment address and
codehash constants (generated; aliases the current tag's
`src/generated/<tag>/` snapshot).
- `src/generated/<tag>/CloneFactory.pointers.sol` — Frozen per-release
deploy-pin snapshots: creation code, runtime code, bytecode hash, deployed
address.
- `script/BuildPointers.sol` — Regenerates the snapshot for the current
`[package].version` and the `LibCloneFactoryDeploy` alias.
codehash constants (generated; aliases the rolling `src/generated/candidate/`
snapshot).
- `src/generated/candidate/CloneFactory.pointers.sol` — The rolling snapshot of
what the current source compiles to: creation code, runtime code, bytecode
hash, deployed address. Regenerated on every `BuildPointers` run.
- `src/generated/<tag>/CloneFactory.pointers.sol` — Frozen release records
(`0_1_3`, `0_1_4`, `0_1_5`), each a copy of `candidate` frozen by a release
tag. Never regenerated.
- `script/BuildPointers.sol` — Regenerates `candidate` and the
`LibCloneFactoryDeploy` alias. Never writes a numbered snapshot.
- `script/cut-release.sh` — Regenerates `candidate`, then freezes it as
`src/generated/<tag>/` at release time. The only thing that creates a numbered
snapshot. Regenerating first is what makes the frozen record equal to the pins
the release actually publishes.
- `script/Deploy.sol` — The Zoltu deploy script.

## Solidity Conventions
Expand Down Expand Up @@ -105,20 +114,32 @@ part of the release workflow.

## Releases and versioning

This is a **deploy repo**, not a library repo, so nothing publishes on merge:

- `[package].version` in `foundry.toml` is the **last released** version (it
names the current `src/generated/<tag>/` snapshot), not a next-version slot. A
normal PR does not bump it; only a release moves it.
This is a **deploy repo**, not a library repo, so nothing publishes on merge. It
uses the **rolling-candidate** model:

- `src/generated/candidate/` is the rolling snapshot of what the current source
compiles to. `BuildPointers` rewrites it every run and `LibCloneFactoryDeploy`
aliases it, so the pins consumers import always describe this repo's source.
`testCandidateSelfConsistent` is the gate.
- `[package].version` in `foundry.toml` **does not name a snapshot** and no
Solidity reads it. It is a placeholder that `rainix-tag-release` overwrites
from the pushed tag. Nothing has been released yet — Soldeer has zero
`rain-factory-deploy` revisions.
- A human pushes a `sol-v<version>` tag, which runs `rainix-tag-release`: it
writes the version from the tag into `foundry.toml`, regenerates the snapshot
(`forge script ./script/BuildPointers.sol && forge fmt`), verifies the live
chains match the fresh pins with `forge test`, publishes `rain-factory-deploy`
to Soldeer, and commits the new snapshot back to `main`.
writes the version from the tag into `foundry.toml`, runs
`bash script/cut-release.sh` (which regenerates `candidate`, then copies it to
`src/generated/<tag>/`), verifies the live chains match the pins with
`forge test`, publishes `rain-factory-deploy` to Soldeer, and commits the new
snapshot back to `main`.
- The on-chain deploy happens **before** tagging, via the manual dispatch above;
`rainix-tag-release` never broadcasts, it only attests.
- Existing `src/generated/<tag>/` snapshots are frozen: a release adds a new tag
directory, it never edits or deletes an existing one. CI enforces this.
directory, it never edits or deletes an existing one. CI enforces this. The
gate's tag test is "three `_`-separated numeric parts", so `candidate/` is
outside it and free to roll.
- Because the pin lib tracks `candidate`, changing `CloneFactory`'s bytecode
makes the five `LibCloneFactoryDeployProdTest` fork tests red until that
bytecode is deployed. That is deliberate: deploy before merge.

## CI

Expand Down
67 changes: 58 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,77 @@
# rain.factory.deploy

The **deployment** half of `rain.factory`: the concrete `CloneFactory` contract,
its deployed address + codehash pins (`LibCloneFactoryDeploy`), the frozen
per-tag deploy-pin snapshots under `src/generated/<tag>/`, and the deploy
script.
its deployed address + codehash pins (`LibCloneFactoryDeploy`), the deploy-pin
snapshots under `src/generated/`, and the deploy script.

The **library** half — the `ICloneable*` interfaces — lives in
[`rain.factory`](https://github.com/rainlanguage/rain.factory) and is imported
here as the `rain-factory` Soldeer package. Consumers that need only the
interfaces depend on `rain-factory`; consumers that need the deployed
address/codehash pins depend on `rain-factory-deploy`.

## Snapshots

`src/generated/` holds two kinds of deploy-pin snapshot, both with the same file
shape (`BYTECODE_HASH`, `DEPLOYED_ADDRESS`, `CREATION_CODE`, `RUNTIME_CODE`):

- **`candidate/`** — the rolling snapshot of whatever the current source
compiles to. Regenerated in full by `forge script ./script/BuildPointers.sol`,
committed, and aliased by `LibCloneFactoryDeploy` — so the pins consumers
import always describe the source in this repo. `testCandidateSelfConsistent`
fails if the source changes without regenerating.
- **`0_1_3/`, `0_1_4/`, `0_1_5/`** — frozen release records. Never regenerated;
CI enforces that they are append-only. Each is a copy of `candidate` taken at
the instant a release tag froze it, kept so a consumer pinned to an older
release can still reproduce and verify that deployment.

## Releases

This is a deploy repo: releases are **manual `sol-v*` tags**, not merges.

The on-chain deploy is a separate, human-dispatched step, run **before**
tagging: the `Manual sol artifacts` workflow runs `script/Deploy.sol` for the
`clone-factory` suite. Tagging then runs `rainix-tag-release`, which regenerates
the snapshot for the tagged version, verifies the live chains match those fresh
pins, publishes `rain-factory-deploy` to Soldeer, and commits the frozen
snapshot back to `main`. It never broadcasts a deploy itself.
`clone-factory` suite. Tagging then runs `rainix-tag-release`, which writes the
tag's version into `foundry.toml`, runs `script/cut-release.sh` to regenerate
`src/generated/candidate/` and freeze it as `src/generated/<tag>/`, verifies the
live chains match the pins, publishes `rain-factory-deploy` to Soldeer, and
commits the frozen snapshot back to `main`. It never broadcasts a deploy itself.

The pushed tag decides the version. `[package].version` in `foundry.toml` does
not name a snapshot dir and is not read by any Solidity in this repo — it is a
placeholder that the release workflow overwrites from the tag. **Nothing has
been released from this repo yet**: Soldeer has zero `rain-factory-deploy`
revisions.

### The `sol-v0.1.6` tag, and why it published nothing

`sol-v0.1.6` exists as a tag on `685bb2ba`. Its `rainix-tag-release` run
([30097157490](https://github.com/rainlanguage/rain.factory.deploy/actions/runs/30097157490))
got as far as *Verify live chain matches the fresh pins* and died there — all
five fork tests failed with `vm.createSelectFork: environment variable
<NETWORK>_RPC_URL not found`. The reusable exported the fork endpoints under the
**secret** names (`RPC_URL_<NETWORK>_FORK`), while `[rpc_endpoints]` in
`foundry.toml` reads `${<NETWORK>_RPC_URL}`, so every endpoint resolved to an
empty string. Publish, commit-back and GitHub Release were all skipped, which is
why the tag exists with no revision, no release and no `0_1_6` snapshot behind
it.

That was a defect in `rainix-tag-release`, not in this repo, and it is fixed
upstream: `rainix` now runs an `rpc-preflight` step that binds each env name
foundry actually reads to an endpoint probed healthy at that moment. The next tag
does not hit this.

Two consequences for whoever cuts the first release:

Nothing publishes on merge, so `[package].version` and the frozen
`src/generated/<tag>/` snapshot it names only ever move together.
- **`sol-v0.1.6` is spent.** It names a commit five behind `main` and it is not
what should be released. Cut a fresh tag on the `main` tip instead of reusing
it. `0.1.6` itself is still free on the registry — nothing was ever published
under it.
- **The fork RPCs still gate the release.** The verify step is the repo's own
fork suite, so a release only publishes if the pins resolve on every supported
chain. Those endpoints are currently intermittent (a free-plan `lb.drpc.live`
returning quota and 408 errors), which reds the same suite on ordinary PRs. Get
them healthy before tagging: a transient failure here fails the release, and
the fix is to tag again, not to retry the run.

See rainlanguage/rain.factory#46 for the split rationale.
18 changes: 11 additions & 7 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
[package]
name = "rain-factory-deploy"
# `version` does NOT name a snapshot dir. Snapshots are cut by pushing a
# `sol-vX.Y.Z` tag: rainix-tag-release writes that tag's version here and
# `script/cut-release.sh` freezes `src/generated/candidate/` under it. Nothing
# has ever been published from this repo (Soldeer has zero `rain-factory-deploy`
# revisions), so this value is a placeholder inherited from rain.factory
# numbering; it coincides with `src/generated/0_1_5/` by history, not by rule.
version = "0.1.5"

[profile.default]
Expand All @@ -18,13 +24,11 @@ evm_version = "cancun"
bytecode_hash = "none"
cbor_metadata = false

# 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" },
]
# BuildPointers writes the rolling src/generated/candidate/ snapshot and the
# generated pin lib under src/. It no longer reads foundry.toml: the snapshot dir
# is the constant `candidate`, and only script/cut-release.sh (bash, outside the
# EVM) reads the version. Nothing else in this repo touches the filesystem.
fs_permissions = [{ access = "read-write", path = "./src" }]
libs = ["dependencies"]

[fuzz]
Expand Down
Loading
Loading