From 56408940977a2893cddfbb7922502b31f1a62e0f Mon Sep 17 00:00:00 2001 From: David Meister Date: Sat, 25 Jul 2026 21:38:30 +0000 Subject: [PATCH 1/3] Migrate to the rolling-candidate snapshot model BuildPointers now writes src/generated/candidate/ unconditionally instead of a dir named after [package].version, and LibCloneFactoryDeploy aliases it, so the pins consumers import always describe the current source. script/cut-release.sh freezes candidate as src/generated// at release time and is the only thing that creates a numbered snapshot. LibCloneFactoryDeployCandidateTest ties the candidate snapshot to the source, to its own recorded address, and to the lib. The three existing frozen snapshots are untouched: 0_1_5 is the live headline pin and 0_1_3/0_1_4 hold the audited bytecode, all live on all five chains. Co-Authored-By: Claude Opus 5 (1M context) --- .gas-snapshot | 4 + .github/workflows/package-release.yaml | 13 ++-- CLAUDE.md | 55 +++++++++----- README.md | 37 +++++++--- foundry.toml | 18 +++-- script/BuildPointers.sol | 74 ++++++++++--------- script/cut-release.sh | 53 +++++++++++++ .../candidate/CloneFactory.pointers.sol | 25 +++++++ src/lib/LibCloneFactoryDeploy.sol | 18 +++-- .../lib/LibCloneFactoryDeployCandidate.t.sol | 64 ++++++++++++++++ ...LibCloneFactoryDeployTaggedConstants.t.sol | 13 +++- 11 files changed, 292 insertions(+), 82 deletions(-) create mode 100755 script/cut-release.sh create mode 100644 src/generated/candidate/CloneFactory.pointers.sol create mode 100644 test/src/lib/LibCloneFactoryDeployCandidate.t.sol diff --git a/.gas-snapshot b/.gas-snapshot index 59833c5..9fe3697 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -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) diff --git a/.github/workflows/package-release.yaml b/.github/workflows/package-release.yaml index ee1b505..8923f7c 100644 --- a/.github/workflows/package-release.yaml +++ b/.github/workflows/package-release.yaml @@ -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// 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//, 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: @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 4c277ed..0222eda 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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' ``` @@ -69,13 +71,18 @@ 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//` snapshot). -- `src/generated//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//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` — Freezes `candidate` as `src/generated//` at + release time. The only thing that creates a numbered snapshot. - `script/Deploy.sol` — The Zoltu deploy script. ## Solidity Conventions @@ -105,20 +112,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//` 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` 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 copies `candidate` to + `src/generated//`, then regenerates), 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//` 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 diff --git a/README.md b/README.md index 9e5cdbc..1ae7d09 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ # 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//`, 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 @@ -11,18 +10,38 @@ 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 freeze +`src/generated/candidate/` as `src/generated//`, 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. -Nothing publishes on merge, so `[package].version` and the frozen -`src/generated//` snapshot it names only ever move together. +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. (`sol-v0.1.6` exists as a tag but its release run failed before +publishing anything.) See rainlanguage/rain.factory#46 for the split rationale. diff --git a/foundry.toml b/foundry.toml index ab1ebb2..0a3a1c3 100644 --- a/foundry.toml +++ b/foundry.toml @@ -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] @@ -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] diff --git a/script/BuildPointers.sol b/script/BuildPointers.sol index e108599..d4e3262 100644 --- a/script/BuildPointers.sol +++ b/script/BuildPointers.sol @@ -10,17 +10,20 @@ import {CloneFactory} from "../src/concrete/CloneFactory.sol"; /// @title BuildPointers /// @notice Generates the deterministic-deploy pins for `CloneFactory`: -/// 1. A frozen per-release snapshot `src/generated//CloneFactory.pointers.sol` -/// (`BYTECODE_HASH`, `DEPLOYED_ADDRESS`, `CREATION_CODE`, `RUNTIME_CODE`) for -/// the current `deployTag()`. Historical tags are never regenerated; a -/// release bump writes a new `/` snapshot beside them. -/// 2. `src/lib/LibCloneFactoryDeploy.sol` — the current-release address and -/// codehash, aliased from the current `deployTag()` snapshot so that -/// snapshot stays the single source of truth (never a duplicated literal). -/// Kept in `src/lib` so consumers' import path is stable across releases. +/// 1. The rolling `src/generated/candidate/CloneFactory.pointers.sol` snapshot +/// (`BYTECODE_HASH`, `DEPLOYED_ADDRESS`, `CREATION_CODE`, `RUNTIME_CODE`) of +/// whatever the current source compiles to. Regenerated in full on every +/// run; committed so tests can assert source and snapshot agree. +/// 2. `src/lib/LibCloneFactoryDeploy.sol` — the address and codehash consumers +/// import, aliased from the `candidate` snapshot so that snapshot stays the +/// single source of truth (never a duplicated literal). Kept in `src/lib` so +/// consumers' import path is stable across releases. /// -/// Run as `forge script script/BuildPointers.sol`. Wired into the autopublish -/// `soldeer-generate-cmd` so the pins regenerate born-green on each release. +/// Numbered snapshots (`src/generated/0_1_5/`, …) are frozen release records. +/// This script never writes one and never reads one: the only thing that creates +/// a numbered dir is `script/cut-release.sh` copying `candidate` at release time. +/// +/// Run as `forge script script/BuildPointers.sol`. contract BuildPointers is Script { string constant GEN_LIB_PATH = "src/lib/LibCloneFactoryDeploy.sol"; @@ -31,17 +34,16 @@ contract BuildPointers is Script { // REUSE-IgnoreEnd - /// @notice The canonical release tag. Read from `foundry.toml` - /// `[package].version` — the single source of truth — with dots converted to - /// underscores for the Solidity dir form (`0.1.3` -> `0_1_3`). - function deployTag() internal view returns (string memory) { - string memory version = vm.parseTomlString(vm.readFile("foundry.toml"), ".package.version"); - bytes memory b = bytes(version); - bytes memory out = new bytes(b.length); - for (uint256 i = 0; i < b.length; i++) { - out[i] = b[i] == "." ? bytes1("_") : b[i]; - } - return string(out); + /// @notice The rolling "current source" snapshot tag — always `candidate`, + /// never a version number. `src/generated/candidate/` is regenerated from the + /// current source on every run, so `LibCloneFactoryDeploy` tracks whatever the + /// source currently compiles to. A numbered snapshot is frozen only when a + /// release tag promotes `candidate` (see `script/cut-release.sh`), and is + /// never regenerated here. + string constant CANDIDATE_TAG = "candidate"; + + function deployTag() internal pure returns (string memory) { + return CANDIDATE_TAG; } function addressConstantString(address addr) internal pure returns (string memory) { @@ -58,14 +60,14 @@ contract BuildPointers is Script { function run() external { LibRainDeploy.etchZoltuFactory(vm); - // A fresh next-version slot has no `/` dir yet, and `vm.writeFile` - // won't create one. + // A fresh checkout that has never generated has no `candidate/` dir yet, + // and `vm.writeFile` won't create one. vm.createDir(string.concat("src/generated/", deployTag()), true); bytes memory creationCode = type(CloneFactory).creationCode; address deployed = LibRainDeploy.deployZoltu(creationCode); - // Frozen per-tag snapshot. + // Rolling `candidate` snapshot of the current source. LibFs.buildFileForContract( vm, deployed, @@ -81,14 +83,14 @@ contract BuildPointers is Script { ) ); - // Current-release pin lib. + // Consumer-facing pin lib. genLibCloneFactoryDeploy(); } /// @notice (Re)generate `src/lib/LibCloneFactoryDeploy.sol`, aliasing the - /// current `deployTag()` snapshot's `DEPLOYED_ADDRESS` + `BYTECODE_HASH` as - /// the current-release constants — the snapshot stays the single source of - /// truth (never a duplicated literal). Emitted line-by-line to match the + /// `candidate` snapshot's `DEPLOYED_ADDRESS` + `BYTECODE_HASH` as the + /// consumer-facing constants — the snapshot stays the single source of truth + /// (never a duplicated literal). Emitted line-by-line to match the /// generated-file convention. function genLibCloneFactoryDeploy() internal { string memory importPath = string.concat("../generated/", deployTag(), "/CloneFactory.pointers.sol"); @@ -105,12 +107,18 @@ contract BuildPointers is Script { vm.writeLine(GEN_LIB_PATH, string.concat("} from \"", importPath, "\";")); vm.writeLine(GEN_LIB_PATH, ""); vm.writeLine(GEN_LIB_PATH, "/// @title LibCloneFactoryDeploy"); - vm.writeLine(GEN_LIB_PATH, "/// @notice The deterministic Zoltu deploy address and code hash of the current"); - vm.writeLine(GEN_LIB_PATH, "/// `CloneFactory` release, aliased from the frozen per-release snapshot in"); - vm.writeLine(GEN_LIB_PATH, "/// `src/generated//CloneFactory.pointers.sol` so that snapshot stays the"); - vm.writeLine(GEN_LIB_PATH, "/// single source of truth. Lets consumers verify/deploy against a precommitted"); - vm.writeLine(GEN_LIB_PATH, "/// address + hash rather than a registry."); + vm.writeLine(GEN_LIB_PATH, "/// @notice The deterministic Zoltu deploy address and code hash of the"); + vm.writeLine(GEN_LIB_PATH, "/// `CloneFactory` the current source compiles to, aliased from the rolling"); + vm.writeLine(GEN_LIB_PATH, "/// `src/generated/candidate/CloneFactory.pointers.sol` snapshot so that snapshot"); + vm.writeLine(GEN_LIB_PATH, "/// stays the single source of truth. Lets consumers verify/deploy against a"); + vm.writeLine(GEN_LIB_PATH, "/// precommitted address + hash rather than a registry. A published release is a"); + vm.writeLine(GEN_LIB_PATH, "/// frozen copy of these bytes, so a consumer pinning a Soldeer version gets an"); + vm.writeLine(GEN_LIB_PATH, "/// immutable pin even though `candidate` rolls on `main`."); vm.writeLine(GEN_LIB_PATH, "library LibCloneFactoryDeploy {"); + vm.writeLine(GEN_LIB_PATH, " /// @dev The snapshot dir these constants are aliased from. Always the"); + vm.writeLine(GEN_LIB_PATH, " /// rolling `candidate`, never a version number."); + vm.writeLine(GEN_LIB_PATH, string.concat(" string constant DEPLOY_TAG = \"", deployTag(), "\";")); + vm.writeLine(GEN_LIB_PATH, ""); vm.writeLine(GEN_LIB_PATH, " address constant CLONE_FACTORY_DEPLOYED_ADDRESS = CLONE_FACTORY_ADDR;"); vm.writeLine(GEN_LIB_PATH, " bytes32 constant CLONE_FACTORY_DEPLOYED_CODEHASH = CLONE_FACTORY_HASH;"); vm.writeLine(GEN_LIB_PATH, "}"); diff --git a/script/cut-release.sh b/script/cut-release.sh new file mode 100755 index 0000000..f3098ae --- /dev/null +++ b/script/cut-release.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LicenseRef-DCL-1.0 +# SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +# Freeze the rolling `candidate` snapshot as a numbered release snapshot, then +# regenerate the deploy pointer lib. +# +# Invoked by rainix-tag-release as its `snapshot-generate-cmd`, AFTER the +# reusable has resolved the release version from the pushed `sol-vX.Y.Z` tag and +# written it to `foundry.toml` `[package].version`. So the version is read from +# `foundry.toml` here — the single source of truth at this point. +# +# Model: `src/generated/candidate/` is the rolling snapshot of what the current +# source compiles to (regenerated every BuildPointers run). A numbered snapshot +# (`0_1_5/`, …) is a FROZEN copy of `candidate` taken at the instant a tag +# releases it — it never changes again (the frozen-snapshots-append-only gate +# enforces this). This script performs that copy, then re-runs BuildPointers so +# the pointer lib is regenerated on top of the freshly cut tree. +set -euo pipefail + +VERSION="$(grep -m1 -E '^version = ' foundry.toml | sed -E 's/^version = "([^"]+)"/\1/')" +if [ -z "$VERSION" ]; then + echo "cut-release: could not read [package].version from foundry.toml" >&2 + exit 1 +fi +# Strict X.Y.Z only: anything else (rc/pre-release suffixes, extra components) +# would freeze a dir like `0_1_7-rc1` that the append-only gate's numeric tag +# filter ignores forever — an orphan snapshot nothing protects. Refuse instead. +if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "cut-release: version '${VERSION}' is not strict X.Y.Z — refusing to cut a snapshot the append-only gate would ignore" >&2 + exit 1 +fi +TAG="${VERSION//./_}" + +if [ ! -d src/generated/candidate ]; then + echo "cut-release: src/generated/candidate is missing — nothing to freeze" >&2 + exit 1 +fi + +# Frozen releases are append-only: re-cutting an existing version must fail +# loudly rather than clobber a frozen snapshot with candidate. +if [ -d "src/generated/${TAG}" ]; then + echo "cut-release: src/generated/${TAG} already exists — refusing to overwrite a frozen release snapshot" >&2 + exit 1 +fi + +echo "cut-release: freezing candidate -> src/generated/${TAG}" +cp -r src/generated/candidate "src/generated/${TAG}" + +# Regenerate candidate (idempotent — source unchanged) and the pointer lib. The +# lib keeps pointing at `candidate`: "current" never advances to the numbered +# dir, which is only a historical record of what this release shipped. +forge script ./script/BuildPointers.sol +forge fmt diff --git a/src/generated/candidate/CloneFactory.pointers.sol b/src/generated/candidate/CloneFactory.pointers.sol new file mode 100644 index 0000000..f0b9c14 --- /dev/null +++ b/src/generated/candidate/CloneFactory.pointers.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity ^0.8.25; + +// THIS FILE IS AUTOGENERATED BY ./script/BuildPointers.sol + +// This file is committed to the repository because there is a circular +// dependency between the contract and its pointers file. The contract +// needs the pointers file to exist so that it can compile, and the pointers +// file needs the contract to exist so that it can be compiled. + +/// @dev Hash of the known bytecode. +bytes32 constant BYTECODE_HASH = bytes32(0x3ec02e2f27d73f603f5b37a7dc228956004e9154c6d1f9c5335bd60036379d00); + +/// @dev The deterministic deploy address of the contract when deployed via +/// the Zoltu factory. +address constant DEPLOYED_ADDRESS = address(0x7053c1D000192f1FeC1fbe20350139F3300dBBaD); + +/// @dev The creation bytecode of the contract. +bytes constant CREATION_CODE = + hex"6080604052348015600e575f80fd5b506105708061001c5f395ff3fe608060405234801561000f575f80fd5b5060043610610034575f3560e01c806340419eec1461003857806393a7e71114610074575b5f80fd5b61004b6100463660046103f1565b610087565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b61004b610082366004610473565b6100c7565b5f61009185610159565b5f6100ae866100a933865f9182526020526040902090565b6101ac565b90506100bd86828787876101b8565b9695505050505050565b5f61014f846100df84865f9182526020526040902090565b6040513060388201526f5af43d82803e903d91602b57fd5bf3ff60248201526014810192909252733d602d80600a3d3981f3363d3d373d3d3d363d73825260588201526037600c8201206078820152605560439091012073ffffffffffffffffffffffffffffffffffffffff1690565b90505b9392505050565b8073ffffffffffffffffffffffffffffffffffffffff163b5f036101a9576040517ff432283200000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b50565b5f61015283835f6102f1565b5f7f3b3e5b48cfaf4a4b3b2b36425ddec21c2c507fd502d2aea55eb7ffa36b0ca2053387878588886040516101f2969594939291906104f3565b60405180910390a16040517f439fab910000000000000000000000000000000000000000000000000000000081527fe0e57eda3f08f2a93bbe980d3df7f9c315eac41181f58b865a13d917fe769fc39073ffffffffffffffffffffffffffffffffffffffff87169063439fab91906102709088908890600401610546565b6020604051808303815f875af115801561028c573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906102b09190610559565b146102e7576040517f19b991a800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5092949350505050565b5f81471015610339576040517fcf4791810000000000000000000000000000000000000000000000000000000081524760048201526024810183905260440160405180910390fd5b763d602d80600a3d3981f3363d3d373d3d3d363d730000008460601b60e81c175f526e5af43d82803e903d91602b57fd5bf38460781b17602052826037600984f5905073ffffffffffffffffffffffffffffffffffffffff8116610152576040517fb06ebf3d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b803573ffffffffffffffffffffffffffffffffffffffff811681146103ec575f80fd5b919050565b5f805f8060608587031215610404575f80fd5b61040d856103c9565b9350602085013567ffffffffffffffff80821115610429575f80fd5b818701915087601f83011261043c575f80fd5b81358181111561044a575f80fd5b88602082850101111561045b575f80fd5b95986020929092019750949560400135945092505050565b5f805f60608486031215610485575f80fd5b61048e846103c9565b9250602084013591506104a3604085016103c9565b90509250925092565b81835281816020850137505f602082840101525f60207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f840116840101905092915050565b5f73ffffffffffffffffffffffffffffffffffffffff8089168352808816602084015280871660408401525084606083015260a0608083015261053a60a0830184866104ac565b98975050505050505050565b602081525f61014f6020830184866104ac565b5f60208284031215610569575f80fd5b505191905056"; + +/// @dev The runtime bytecode of the contract. +bytes constant RUNTIME_CODE = + hex"608060405234801561000f575f80fd5b5060043610610034575f3560e01c806340419eec1461003857806393a7e71114610074575b5f80fd5b61004b6100463660046103f1565b610087565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b61004b610082366004610473565b6100c7565b5f61009185610159565b5f6100ae866100a933865f9182526020526040902090565b6101ac565b90506100bd86828787876101b8565b9695505050505050565b5f61014f846100df84865f9182526020526040902090565b6040513060388201526f5af43d82803e903d91602b57fd5bf3ff60248201526014810192909252733d602d80600a3d3981f3363d3d373d3d3d363d73825260588201526037600c8201206078820152605560439091012073ffffffffffffffffffffffffffffffffffffffff1690565b90505b9392505050565b8073ffffffffffffffffffffffffffffffffffffffff163b5f036101a9576040517ff432283200000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b50565b5f61015283835f6102f1565b5f7f3b3e5b48cfaf4a4b3b2b36425ddec21c2c507fd502d2aea55eb7ffa36b0ca2053387878588886040516101f2969594939291906104f3565b60405180910390a16040517f439fab910000000000000000000000000000000000000000000000000000000081527fe0e57eda3f08f2a93bbe980d3df7f9c315eac41181f58b865a13d917fe769fc39073ffffffffffffffffffffffffffffffffffffffff87169063439fab91906102709088908890600401610546565b6020604051808303815f875af115801561028c573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906102b09190610559565b146102e7576040517f19b991a800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5092949350505050565b5f81471015610339576040517fcf4791810000000000000000000000000000000000000000000000000000000081524760048201526024810183905260440160405180910390fd5b763d602d80600a3d3981f3363d3d373d3d3d363d730000008460601b60e81c175f526e5af43d82803e903d91602b57fd5bf38460781b17602052826037600984f5905073ffffffffffffffffffffffffffffffffffffffff8116610152576040517fb06ebf3d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b803573ffffffffffffffffffffffffffffffffffffffff811681146103ec575f80fd5b919050565b5f805f8060608587031215610404575f80fd5b61040d856103c9565b9350602085013567ffffffffffffffff80821115610429575f80fd5b818701915087601f83011261043c575f80fd5b81358181111561044a575f80fd5b88602082850101111561045b575f80fd5b95986020929092019750949560400135945092505050565b5f805f60608486031215610485575f80fd5b61048e846103c9565b9250602084013591506104a3604085016103c9565b90509250925092565b81835281816020850137505f602082840101525f60207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f840116840101905092915050565b5f73ffffffffffffffffffffffffffffffffffffffff8089168352808816602084015280871660408401525084606083015260a0608083015261053a60a0830184866104ac565b98975050505050505050565b602081525f61014f6020830184866104ac565b5f60208284031215610569575f80fd5b505191905056"; diff --git a/src/lib/LibCloneFactoryDeploy.sol b/src/lib/LibCloneFactoryDeploy.sol index d63d7ef..9dfbc3d 100644 --- a/src/lib/LibCloneFactoryDeploy.sol +++ b/src/lib/LibCloneFactoryDeploy.sol @@ -7,15 +7,21 @@ pragma solidity ^0.8.25; import { DEPLOYED_ADDRESS as CLONE_FACTORY_ADDR, BYTECODE_HASH as CLONE_FACTORY_HASH -} from "../generated/0_1_5/CloneFactory.pointers.sol"; +} from "../generated/candidate/CloneFactory.pointers.sol"; /// @title LibCloneFactoryDeploy -/// @notice The deterministic Zoltu deploy address and code hash of the current -/// `CloneFactory` release, aliased from the frozen per-release snapshot in -/// `src/generated//CloneFactory.pointers.sol` so that snapshot stays the -/// single source of truth. Lets consumers verify/deploy against a precommitted -/// address + hash rather than a registry. +/// @notice The deterministic Zoltu deploy address and code hash of the +/// `CloneFactory` the current source compiles to, aliased from the rolling +/// `src/generated/candidate/CloneFactory.pointers.sol` snapshot so that snapshot +/// stays the single source of truth. Lets consumers verify/deploy against a +/// precommitted address + hash rather than a registry. A published release is a +/// frozen copy of these bytes, so a consumer pinning a Soldeer version gets an +/// immutable pin even though `candidate` rolls on `main`. library LibCloneFactoryDeploy { + /// @dev The snapshot dir these constants are aliased from. Always the + /// rolling `candidate`, never a version number. + string constant DEPLOY_TAG = "candidate"; + address constant CLONE_FACTORY_DEPLOYED_ADDRESS = CLONE_FACTORY_ADDR; bytes32 constant CLONE_FACTORY_DEPLOYED_CODEHASH = CLONE_FACTORY_HASH; } diff --git a/test/src/lib/LibCloneFactoryDeployCandidate.t.sol b/test/src/lib/LibCloneFactoryDeployCandidate.t.sol new file mode 100644 index 0000000..e41d4dd --- /dev/null +++ b/test/src/lib/LibCloneFactoryDeployCandidate.t.sol @@ -0,0 +1,64 @@ +// 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-1.16.1/src/Test.sol"; +import {LibRainDeploy} from "rain-deploy-0.1.3/src/lib/LibRainDeploy.sol"; +import {LibCloneFactoryDeploy} from "../../../src/lib/LibCloneFactoryDeploy.sol"; +import {CloneFactory} from "../../../src/concrete/CloneFactory.sol"; +import { + BYTECODE_HASH as CLONE_FACTORY_BYTECODE_HASH_CANDIDATE, + DEPLOYED_ADDRESS as CLONE_FACTORY_DEPLOYED_ADDRESS_CANDIDATE, + CREATION_CODE as CLONE_FACTORY_CREATION_CODE_CANDIDATE, + RUNTIME_CODE as CLONE_FACTORY_RUNTIME_CODE_CANDIDATE +} from "../../../src/generated/candidate/CloneFactory.pointers.sol"; + +/// @title LibCloneFactoryDeployCandidateTest +/// @notice The rolling `src/generated/candidate/` snapshot is the one snapshot +/// that is NOT frozen: it is regenerated from the current source on every +/// `forge script ./script/BuildPointers.sol` run, and `LibCloneFactoryDeploy` +/// aliases it. These tests pin the whole chain that makes that safe: +/// +/// current source -> candidate `CREATION_CODE` -> candidate `DEPLOYED_ADDRESS` +/// -> `LibCloneFactoryDeploy.CLONE_FACTORY_DEPLOYED_ADDRESS` +/// +/// Break any link — edit `CloneFactory` without regenerating, hand-edit a pin, +/// or point the generated lib at a different snapshot dir — and one of these +/// fails. Pure forge: no git, no shell, no `foundry.toml` parse, no chain. +contract LibCloneFactoryDeployCandidateTest is Test { + /// The committed `candidate` snapshot MUST match the current source: the + /// stored `CREATION_CODE` constant equals `type(CloneFactory).creationCode`. + /// Fails if the source changes without regenerating `candidate` via + /// `forge script ./script/BuildPointers.sol`. + function testCandidateSelfConsistent() external pure { + assertEq(CLONE_FACTORY_CREATION_CODE_CANDIDATE, type(CloneFactory).creationCode); + } + + /// `LibCloneFactoryDeploy` — the lib consumers import — must alias the + /// `candidate` snapshot and say so. Catches the generated lib being left + /// pointing at a frozen numbered snapshot while `candidate` moves on, which + /// would silently publish a stale address as the headline pin. + function testCandidateIsTheAliasedSnapshot() external pure { + assertEq(LibCloneFactoryDeploy.DEPLOY_TAG, "candidate"); + assertEq(LibCloneFactoryDeploy.CLONE_FACTORY_DEPLOYED_ADDRESS, CLONE_FACTORY_DEPLOYED_ADDRESS_CANDIDATE); + assertEq(LibCloneFactoryDeploy.CLONE_FACTORY_DEPLOYED_CODEHASH, CLONE_FACTORY_BYTECODE_HASH_CANDIDATE); + } + + /// `keccak256(RUNTIME_CODE) == BYTECODE_HASH` for the candidate — the pin is + /// internally consistent, exactly as the frozen tags are required to be. + function testCandidateRuntimeHashesToBytecodeHash() external pure { + assertEq(keccak256(CLONE_FACTORY_RUNTIME_CODE_CANDIDATE), CLONE_FACTORY_BYTECODE_HASH_CANDIDATE); + } + + /// Deploying the candidate's recorded `CREATION_CODE` via the Zoltu factory + /// lands at its recorded `DEPLOYED_ADDRESS` with the recorded codehash — the + /// snapshot reproduces its own deployment, so freezing it at release time + /// records a reproducible deployment. + function testCandidateCreationDeploysToPinnedAddress() external { + LibRainDeploy.etchZoltuFactory(vm); + address deployed = LibRainDeploy.deployZoltu(CLONE_FACTORY_CREATION_CODE_CANDIDATE); + assertEq(deployed, CLONE_FACTORY_DEPLOYED_ADDRESS_CANDIDATE); + assertEq(deployed.codehash, CLONE_FACTORY_BYTECODE_HASH_CANDIDATE); + assertEq(keccak256(deployed.code), CLONE_FACTORY_BYTECODE_HASH_CANDIDATE); + } +} diff --git a/test/src/lib/LibCloneFactoryDeployTaggedConstants.t.sol b/test/src/lib/LibCloneFactoryDeployTaggedConstants.t.sol index a13cb31..89dcbda 100644 --- a/test/src/lib/LibCloneFactoryDeployTaggedConstants.t.sol +++ b/test/src/lib/LibCloneFactoryDeployTaggedConstants.t.sol @@ -24,11 +24,18 @@ import { } from "../../../src/generated/0_1_5/CloneFactory.pointers.sol"; /// @title LibCloneFactoryDeployTaggedConstantsTest -/// @notice Each frozen per-tag `CloneFactory` snapshot must be self-consistent +/// @notice Each FROZEN per-tag `CloneFactory` snapshot must be self-consistent /// and reproducible: its recorded `BYTECODE_HASH` is the keccak of its recorded /// `RUNTIME_CODE`, and Zoltu-deploying its recorded `CREATION_CODE` lands at the -/// recorded `DEPLOYED_ADDRESS` with the recorded codehash. A new release adds a -/// tag import + a test pair here. +/// recorded `DEPLOYED_ADDRESS` with the recorded codehash. Every tag here is a +/// live production deployment on all five supported chains, so these stay for as +/// long as the tags do — a tag whose record no longer reproduces is a tag a +/// pinned consumer can no longer verify. Cutting a release adds a tag import + +/// test pair here. +/// +/// The rolling `candidate` snapshot is covered separately by +/// `LibCloneFactoryDeployCandidateTest`, which additionally ties it to the +/// current source. contract LibCloneFactoryDeployTaggedConstantsTest is Test { /// `keccak256(RUNTIME_CODE) == BYTECODE_HASH` for the tag — the pin is /// internally consistent. From 7288d302364ee9305dc78c3280d93c298ff333e0 Mon Sep 17 00:00:00 2001 From: David Meister Date: Thu, 13 Aug 2026 09:31:41 +0000 Subject: [PATCH 2/3] cut-release: regenerate candidate before freezing it, not after MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit script/cut-release.sh is what rainix-tag-release runs as its snapshot-generate-cmd, so this is the code that will cut this repo's first real release (rain.factory#46 step 3). It froze the numbered snapshot from the committed candidate and only then re-ran BuildPointers. That order is silently wrong whenever the committed candidate has drifted from the source. The copy freezes the stale bytes into src/generated//, which the frozen-snapshots-append-only gate then protects forever, and the regeneration immediately moves candidate on to the real ones. The release publishes a LibCloneFactoryDeploy aliasing the new address while permanently recording the old one as what that version shipped. Nothing downstream catches it. testCandidateSelfConsistent compares the REGENERATED candidate to the source, so it is green either way, and no test compares a numbered dir to candidate. The append-only gate only sees an added dir. The failure is invisible until someone tries to reproduce the release. Reordered so the frozen dir is a copy of a candidate already known to match the source, with forge fmt also moved ahead of the copy so the two are byte-identical rather than differing by formatting. Added a diff -r assertion after the copy so the property is checked, not assumed. No behaviour change when candidate is already fresh, which is the normal case CI enforces on main. This is the abnormal case: tagging while main is red. Docs corrected to match — CLAUDE.md stated the old order in two places. --- CLAUDE.md | 14 ++++++++------ script/cut-release.sh | 37 ++++++++++++++++++++++++++++--------- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0222eda..f914688 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -81,8 +81,10 @@ as the `rain-factory` Soldeer dependency, so they are read under tag. Never regenerated. - `script/BuildPointers.sol` — Regenerates `candidate` and the `LibCloneFactoryDeploy` alias. Never writes a numbered snapshot. -- `script/cut-release.sh` — Freezes `candidate` as `src/generated//` at - release time. The only thing that creates a numbered snapshot. +- `script/cut-release.sh` — Regenerates `candidate`, then freezes it as + `src/generated//` 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 @@ -125,10 +127,10 @@ uses the **rolling-candidate** model: `rain-factory-deploy` revisions. - A human pushes a `sol-v` tag, which runs `rainix-tag-release`: it writes the version from the tag into `foundry.toml`, runs - `bash script/cut-release.sh` (which copies `candidate` to - `src/generated//`, then regenerates), verifies the live chains match the - pins with `forge test`, publishes `rain-factory-deploy` to Soldeer, and - commits the new snapshot back to `main`. + `bash script/cut-release.sh` (which regenerates `candidate`, then copies it to + `src/generated//`), 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//` snapshots are frozen: a release adds a new tag diff --git a/script/cut-release.sh b/script/cut-release.sh index f3098ae..9d39dde 100755 --- a/script/cut-release.sh +++ b/script/cut-release.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash # SPDX-License-Identifier: LicenseRef-DCL-1.0 # SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd -# Freeze the rolling `candidate` snapshot as a numbered release snapshot, then -# regenerate the deploy pointer lib. +# Regenerate the rolling `candidate` snapshot, then freeze it as a numbered +# release snapshot. # # Invoked by rainix-tag-release as its `snapshot-generate-cmd`, AFTER the # reusable has resolved the release version from the pushed `sol-vX.Y.Z` tag and @@ -13,8 +13,8 @@ # source compiles to (regenerated every BuildPointers run). A numbered snapshot # (`0_1_5/`, …) is a FROZEN copy of `candidate` taken at the instant a tag # releases it — it never changes again (the frozen-snapshots-append-only gate -# enforces this). This script performs that copy, then re-runs BuildPointers so -# the pointer lib is regenerated on top of the freshly cut tree. +# enforces this). This script regenerates `candidate` from the current source and +# then performs that copy. set -euo pipefail VERSION="$(grep -m1 -E '^version = ' foundry.toml | sed -E 's/^version = "([^"]+)"/\1/')" @@ -43,11 +43,30 @@ if [ -d "src/generated/${TAG}" ]; then exit 1 fi +# Regenerate FIRST, freeze SECOND. The numbered dir must record what this release +# actually publishes, and what it publishes is `candidate` — the pin lib aliases +# it — so the copy has to come from a `candidate` already known to match the +# current source. +# +# The other order is silently wrong whenever the committed `candidate` has +# drifted from the source: the copy freezes the stale bytes into a dir the +# append-only gate then protects forever, while this regeneration moves +# `candidate` on to the real ones. Nothing downstream catches it — +# `testCandidateSelfConsistent` checks the *regenerated* `candidate` against the +# source, and no test compares a numbered dir to `candidate` — so the release +# publishes one address and permanently records another. +# +# `forge fmt` also runs before the copy, so the frozen dir is byte-identical to +# `candidate` rather than to its pre-format form. +forge script ./script/BuildPointers.sol +forge fmt + echo "cut-release: freezing candidate -> src/generated/${TAG}" cp -r src/generated/candidate "src/generated/${TAG}" -# Regenerate candidate (idempotent — source unchanged) and the pointer lib. The -# lib keeps pointing at `candidate`: "current" never advances to the numbered -# dir, which is only a historical record of what this release shipped. -forge script ./script/BuildPointers.sol -forge fmt +# The point of the ordering above, asserted rather than assumed: the frozen +# record and the published pin are the same bytes. +if ! diff -r src/generated/candidate "src/generated/${TAG}"; then + echo "cut-release: src/generated/${TAG} does not match src/generated/candidate after the copy" >&2 + exit 1 +fi From a4548f9f3c204e4c0cb1a3d11c7da4f2580f8993 Mon Sep 17 00:00:00 2001 From: David Meister Date: Thu, 13 Aug 2026 09:39:57 +0000 Subject: [PATCH 3/3] README: record why sol-v0.1.6 published nothing, and what that means for the first release This branch's README already said "sol-v0.1.6 exists as a tag but its release run failed before publishing anything" without saying why. The why matters, because cutting the first real release (rain.factory#46 step 3) is the next human action in this repo and the same failure would eat it. Run 30097157490 reached "Verify live chain matches the fresh pins" and died with all five fork tests reporting `vm.createSelectFork: environment variable _RPC_URL not found`. rainix-tag-release exported the endpoints under the SECRET names (RPC_URL__FORK) while foundry.toml's [rpc_endpoints] read the _RPC_URL form, so every one resolved empty. Publish, commit-back and GitHub Release were skipped -- hence a tag with no revision, no release and no 0_1_6 snapshot. That was a rainix defect, since fixed there: the reusable now runs rpc-preflight, binding each env name foundry reads to an endpoint probed healthy at that moment. Recorded with the two consequences a releaser needs: sol-v0.1.6 is spent (it names a commit five behind main; cut a fresh tag, though the 0.1.6 version number is still free on the registry since nothing published), and the fork RPCs still gate the release -- they are currently intermittent, and a transient failure there means tagging again, not rerunning. Also corrected the cut-release.sh description one paragraph up to match the regenerate-then-freeze order this branch now uses. --- README.md | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1ae7d09..4b7301c 100644 --- a/README.md +++ b/README.md @@ -32,16 +32,46 @@ 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 writes the -tag's version into `foundry.toml`, runs `script/cut-release.sh` to freeze -`src/generated/candidate/` as `src/generated//`, 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. +tag's version into `foundry.toml`, runs `script/cut-release.sh` to regenerate +`src/generated/candidate/` and freeze it as `src/generated//`, 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. (`sol-v0.1.6` exists as a tag but its release run failed before -publishing anything.) +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 +_RPC_URL not found`. The reusable exported the fork endpoints under the +**secret** names (`RPC_URL__FORK`), while `[rpc_endpoints]` in +`foundry.toml` reads `${_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: + +- **`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.