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..f914688 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,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//` 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` — 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 @@ -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//` 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 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 - 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..4b7301c 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,68 @@ 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//`, 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 +_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: -Nothing publishes on merge, so `[package].version` and the frozen -`src/generated//` 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. 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..9d39dde --- /dev/null +++ b/script/cut-release.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LicenseRef-DCL-1.0 +# SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +# 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 +# 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 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/')" +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 + +# 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}" + +# 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 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.