Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions .claude/rules/mutation-profile.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@ paths:
nix develop -c bash -c 'FOUNDRY_PROFILE=mutation forge test'
```

`ExtrospectConstantsTest` (`test/src/concrete/Extrospect.constants.t.sol`) pins
`type(Extrospect).creationCode` and `type(Extrospect).runtimeCode` against the
`EXTROSPECT_*_V1` constants. Those compiler outputs change for any edit to any
source file reachable from `Extrospect`, so `testExtrospectCreationBytecode` and
`testExtrospectRuntimeCodehash` both fail under every source mutation, whether
or not the mutated behaviour is observable. A campaign run on the default
profile scores every mutant `KILLED` and measures nothing.
`ExtrospectConstantsTest` (`test/src/concrete/Extrospect.constants.t.sol`) ties
the generated candidate snapshot to `type(Extrospect).creationCode` and
`type(Extrospect).runtimeCode`, and `ExtrospectDeploySnapshotTest`
(`test/src/abstract/ExtrospectDeploySnapshot.t.sol`) inherits
`testSnapshotMatchesSource` and `testSnapshotInternallyConsistent`, which anchor
the same snapshot to source. Those compiler outputs change for any edit to any
source file reachable from `Extrospect`, so both contracts fail under every
source mutation, whether or not the mutated behaviour is observable. A campaign
run on the default profile scores every mutant `KILLED` and measures nothing.

`[profile.mutation]` in `foundry.toml` sets
`no_match_contract = "ExtrospectConstantsTest"` and inherits everything else
from `default`. The default profile keeps the pins, so CI and releases still
catch constant drift.
`no_match_contract = "ExtrospectConstantsTest|ExtrospectDeploySnapshotTest"` and
inherits everything else from `default`. The default profile keeps the pins, so
CI and releases still catch snapshot drift.
20 changes: 20 additions & 0 deletions .github/workflows/package-release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Package Release
# Deploy repo: a manual `sol-v*` tag is the sole release trigger.
# rainix-autopublish's merge-driven, next-version lifecycle is wrong for this
# shape: it bumps the release version on every merge, while the frozen deploy
# tag only advances at deploy time.
#
# The on-chain deploy is separate and manual, run BEFORE tagging; this never
# broadcasts. rainix-tag-release's chain verification is the intended gate: a
# release is declared here only once it is a deployment that already happened.
on:
push:
tags:
- sol-v*
jobs:
release:
uses: rainlanguage/rainix/.github/workflows/rainix-tag-release.yaml@main
with:
soldeer-package: rain-extrospection-deploy
snapshot-generate-cmd: forge script ./script/Build.sol --sig "cutRelease()" && forge fmt
secrets: inherit
Comment thread
coderabbitai[bot] marked this conversation as resolved.
4 changes: 2 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"editor.rulers": [80]
}
"editor.rulers": [80]
}
31 changes: 25 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,28 @@ Mutation and coverage campaigns: see `.claude/rules/mutation-profile.md`.

## Layout

- `src/concrete/Extrospect.sol` — the deployed contract, and the three deploy
pins (`EXTROSPECT_ZOLTU_ADDRESS_V1`, `EXTROSPECT_RUNTIME_CODEHASH_V1`,
`EXTROSPECT_CREATION_BYTECODE_V1`) as file-level constants.
- `script/Deploy.sol` — the Zoltu deploy script for the `extrospect` suite.
- `script/PrintExtrospectAddress.sol` — emits the pinned address for CI.
- `src/concrete/Extrospect.sol` — the deployed contract.
- `src/abstract/ExtrospectDeploySuites.sol` — the ONE declaration of what this
repo deploys, inherited by the deploy script, the build script and the
verification tests. `src/abstract/RainDeploySuitesBase.sol` re-exports the
shipped base so the generated libs' `../abstract/` import resolves.
- `src/generated/candidate/Extrospect.sol` — the rolling deploy snapshot
(address, codehash, creation + runtime bytecode, dependencies), written by
`script/Build.sol`. NEVER edit generated files by hand. `src/generated/<tag>/`
directories are frozen release records, written once by `cutRelease()`.
- `src/lib/` — generated: `LibExtrospectDeploy` (alias over the candidate pins),
`LibExtrospectReleased` and `LibReleasedSuites` (the released declaration,
emitted from the frozen record).
- `script/Build.sol` — regenerates the snapshot and libs (`run()`), freezes a
release record (`cutRelease()`).
- `script/Deploy.sol` — the declaration plus `RainDeployBroadcast`; broadcasts
the suite `DEPLOYMENT_SUITE` names.
- `script/PrintExtrospectAddress.sol` — emits the recorded address for CI.
- `test/src/concrete/` — mirrors `src/` by subject path; files are named
`Extrospect.<functionName>.t.sol`.
- `test/src/abstract/` — `ExtrospectDeploySnapshotTest` and
`ExtrospectDeployChainTest` bind the declaration to the shipped
`RainDeployVerifySnapshot` / `RainDeployVerifyChain` abstracts.
- `test/concrete/` — test-only fixtures. `MockBeacon`, `EmptyContract` and
`SolidityCBORFixture` also exist in `rain.extrospection`, which needs them for
its library tests.
Expand All @@ -61,4 +76,8 @@ Mutation and coverage campaigns: see `.claude/rules/mutation-profile.md`.

Deployed via the Zoltu deployer, so the address is a pure function of the
bytecode. A deploy is a human-dispatched run of `Manual sol artifacts`, never a
merge. There is no release workflow yet and nothing is published: see README.md.
merge, and comes BEFORE the tag. Releases are manual `sol-v*` tags:
`package-release.yaml` runs `rainix-tag-release`, which cuts the frozen
`src/generated/<tag>/` record via `script/Build.sol --sig "cutRelease()"` and
publishes `rain-extrospection-deploy` to Soldeer. `releasedSuites()` is empty
until the first release is cut. See README.md.
61 changes: 40 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
# rain.extrospection.deploy

The **deployment** half of `rain.extrospection`: the concrete `Extrospect`
contract, its pinned deploy record (`EXTROSPECT_ZOLTU_ADDRESS_V1`,
`EXTROSPECT_RUNTIME_CODEHASH_V1`, `EXTROSPECT_CREATION_BYTECODE_V1`), the
per-function equivalence tests that hold `Extrospect` to the libraries it
forwards to, and the deploy script.
contract, its generated deploy record (`src/generated/candidate/Extrospect.sol`
and the `LibExtrospectDeploy` alias lib), the per-function equivalence tests
that hold `Extrospect` to the libraries it forwards to, and the deploy script.

The **library** half — `IExtrospectV1`, `IBeacon`, `IOwnable` and the
`LibExtrospect*` libraries — lives in
Expand All @@ -16,25 +15,45 @@ that need the deployed address, codehash or creation bytecode depend on

## The deploy record

`src/concrete/Extrospect.sol` carries all three pins as file-level constants.
`ExtrospectConstantsTest` holds each against the compiler and then executes the
pinned creation bytecode through the Zoltu factory's own bytecode, etched
offline, asserting it lands at the pinned address and leaves code hashing to the
pinned codehash. The whole record is a pure function of the creation code, so
that check needs no network.
This repo carries the
[`rain.deploy`](https://github.com/rainlanguage/rain.deploy) deploy standard,
generated rather than hand-written:

- `src/abstract/ExtrospectDeploySuites.sol` declares everything this repo
deploys, ONCE: the rolling `extrospect` candidate anchored to
`type(Extrospect).creationCode`, and the released suites read from the
generated `LibReleasedSuites` — empty until the first release is cut.
- `src/generated/candidate/Extrospect.sol` is the rolling snapshot
`script/Build.sol` regenerates from current source: deterministic Zoltu
address, codehash, creation bytecode, runtime bytecode and dependency list.
`src/lib/LibExtrospectDeploy.sol` aliases its pins under a stable import path.
- `script/Deploy.sol` is the declaration plus `RainDeployBroadcast`, nothing
else; `script/Build.sol` is the declaration plus `BuildScript`.

`ExtrospectDeploySnapshotTest` inherits every no-network assertion from
`RainDeployVerifySnapshot`: the record derives from its own creation code, the
candidate is a snapshot of this repo's source, and every frozen
`src/generated/<tag>/` record is declared. `ExtrospectDeployChainTest` inherits
`RainDeployVerifyChain`, which holds every RELEASED suite live on every
supported network — with no release cut it has no subject and forks nothing.
`ExtrospectConstantsTest` ties the generated candidate to the live V1 deployment
(`0x1BE878af679C1a0A6AC15108b0F4398de1f94506`) byte for byte, and executes the
recorded creation bytecode through the Zoltu factory's own bytecode, etched
offline. The whole record is a pure function of the creation code, so none of
that needs a network.

## Releases

Nothing publishes on merge, and there is no release workflow yet, so pushing a
`sol-v*` tag does nothing. `rainix-tag-release` takes a required
`snapshot-generate-cmd` that regenerates a frozen `src/generated/<tag>/` deploy
record, and the library that generates one — `LibRainDeploySnapshot` in
[`rain.deploy`](https://github.com/rainlanguage/rain.deploy) — is on that repo's
`main` and in no published revision (latest `rain-deploy` is 0.1.5, cut
2026-07-30). Wiring the release lane is part of adopting that record, tracked by
rainlanguage/rain.extrospection#44.

The on-chain deploy is separate and human-dispatched: the `Manual sol artifacts`
workflow runs `script/Deploy.sol` for the `extrospect` suite.
Releases are manual `sol-v*` tags, never merges. `package-release.yaml` runs
`rainix-tag-release`, whose `snapshot-generate-cmd` runs
`script/Build.sol --sig "cutRelease()"`: it regenerates the rolling candidate,
freezes it as `src/generated/<tag>/`, regenerates the released-suites libs from
the record that now holds the release, verifies the live chains against the
fresh pins, publishes `rain-extrospection-deploy` to Soldeer and commits the
frozen record back to `main`.

The on-chain deploy is separate and human-dispatched, run BEFORE tagging: the
`Manual sol artifacts` workflow runs `script/Deploy.sol` for the `extrospect`
suite.

See rainlanguage/rain.extrospection#46 for the split rationale.
46 changes: 34 additions & 12 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ out = 'out'
# See more config options https://github.com/foundry-rs/foundry/tree/master/config

# `Extrospect`'s deterministic deploy address, runtime codehash and creation
# bytecode are pinned in `src/concrete/Extrospect.sol` and are a pure function
# of the creation code, which is a function of these settings. They are pinned
# exactly rather than floated so the pins cannot move under a compiler or
# default-target change, and they match the settings `rain.extrospection`
# compiles the same contract with.
# bytecode are recorded in the generated `src/generated/candidate/Extrospect.sol`
# snapshot and are a pure function of the creation code, which is a function of
# these settings. They are pinned exactly rather than floated so the record
# cannot move under a compiler or default-target change, and they match the
# settings `rain.extrospection` compiles the same contract with.
solc = "0.8.25"
optimizer = true
optimizer_runs = 100000
Expand All @@ -32,27 +32,41 @@ bytecode_hash = "none"
cbor_metadata = false
libs = ["dependencies"]

# `script/Build.sol` writes the generated snapshot and libs under `src/`, and
# reads the release version from this file when `cutRelease()` runs.
# `ExtrospectDeploySnapshotTest`'s inherited frozen-record walk reads
# `src/generated/` too, so tests need the same read access.
fs_permissions = [
{ access = "read", path = "./foundry.toml" },
{ access = "read-write", path = "./src" },
]

[fuzz]
runs = 2048

# Behaviour-only view of the test suite, for mutation and coverage campaigns.
# `ExtrospectConstantsTest` pins `type(Extrospect).creationCode` and
# `type(Extrospect).runtimeCode`, which change for any edit to any source file
# reachable from `Extrospect`. Those pins therefore fail under every source
# mutation, observable or not, so a campaign that includes them scores every
# `ExtrospectConstantsTest` ties the generated candidate snapshot to the
# deployed V1 record and to `type(Extrospect).creationCode`/`runtimeCode`, and
# `ExtrospectDeploySnapshotTest`'s inherited assertions anchor the same
# snapshot to source. Compiler outputs change for any edit to any source file
# reachable from `Extrospect`, so both contracts fail under every source
# mutation, observable or not, and a campaign that includes them scores every
# mutant as killed. This profile drops them and inherits everything else from
# `default`.
[profile.mutation]
no_match_contract = "ExtrospectConstantsTest"
no_match_contract = "ExtrospectConstantsTest|ExtrospectDeploySnapshotTest"

# `recursive_deps` is off, so every package an import resolves through is
# declared here, including `rain-solmem` — reached from `rain-extrospection`'s
# `LibExtrospectBytecode`.
[dependencies]
forge-std = "1.16.1"
forge-std = "1.16.2"
"rain-extrospection" = "0.1.6"
"rain-solmem" = "0.1.3"
"rain-deploy" = "0.1.5"
"rain-deploy" = "0.1.7"
# `LibRainDeploySnapshot` — reached from `script/Build.sol` and the shipped
# verify abstracts — emits the generated record via `LibCodeGen`/`LibFs`.
"rain-sol-codegen" = "0.1.36"

[soldeer]
recursive_deps = false
Expand All @@ -63,16 +77,24 @@ recursive_deps = false
arbitrum = "${ARBITRUM_RPC_URL}"
base = "${BASE_RPC_URL}"
base_sepolia = "${BASE_SEPOLIA_RPC_URL}"
ethereum = "${ETHEREUM_RPC_URL}"
flare = "${FLARE_RPC_URL}"
hyperevm = "${HYPEREVM_RPC_URL}"
polygon = "${POLYGON_RPC_URL}"

# `rainix-manual-sol-artifacts` passes `--verify` by default and exports exactly
# these variable names, so a deploy without this section broadcasts and then
# fails with no API key configured for the chain. One entry per
# `[rpc_endpoints]` alias, because the deploy goes to all of them.
#
# "chain" is stated on the entries whose alias foundry does not itself resolve
# to a chain, as foundry raises a config error for an unknown alias with
# neither "chain" nor "url".
[etherscan]
arbitrum = { key = "${CI_DEPLOY_ARBITRUM_ETHERSCAN_API_KEY}" }
base = { key = "${CI_DEPLOY_BASE_ETHERSCAN_API_KEY}" }
base_sepolia = { key = "${CI_DEPLOY_BASE_SEPOLIA_ETHERSCAN_API_KEY}" }
ethereum = { key = "${CI_DEPLOY_ETHEREUM_ETHERSCAN_API_KEY}", chain = 1 }
flare = { key = "${CI_DEPLOY_FLARE_ETHERSCAN_API_KEY}" }
hyperevm = { key = "${CI_DEPLOY_HYPEREVM_ETHERSCAN_API_KEY}", chain = 999 }
polygon = { key = "${CI_DEPLOY_POLYGON_ETHERSCAN_API_KEY}" }
87 changes: 87 additions & 0 deletions script/Build.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// SPDX-License-Identifier: LicenseRef-DCL-1.0
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity =0.8.25;

import {BuildScript} from "rain-deploy-0.1.7/src/abstract/BuildScript.sol";
import {DeployCandidate} from "../src/abstract/RainDeploySuitesBase.sol";
import {ExtrospectDeploySuites} from "../src/abstract/ExtrospectDeploySuites.sol";
import {LibRainDeploySnapshot} from "rain-deploy-0.1.7/src/lib/LibRainDeploySnapshot.sol";

/// One contract's generated files: the rolling snapshot, the alias lib that
/// re-exports its pins and the released-suites lib emitted from its record.
struct GeneratedContract {
/// Places the snapshot inside `src/generated/<dir>/` and names both
/// generated libs.
string contractName;
/// Prefix for the constants the alias lib exports, e.g. `EXTROSPECT`.
string constantPrefix;
/// Snapshots are written from its `sourceCreationCode` and
/// `snapshot.dependencies`; the released lib takes its suite key and
/// artifact path from its `snapshot`.
DeployCandidate candidate;
}

/// @title Build
/// @notice Generates the deploy pins for every contract this repo deploys.
/// `generatedContracts()` is the only list, read by every hook below.
contract Build is BuildScript, ExtrospectDeploySuites {
/// Every contract this repo generates deploy pins for.
/// @return The generated contracts.
function generatedContracts() internal pure returns (GeneratedContract[] memory) {
GeneratedContract[] memory contracts = new GeneratedContract[](1);
contracts[0] = GeneratedContract({
contractName: "Extrospect", constantPrefix: "EXTROSPECT", candidate: extrospectCandidate()
});
return contracts;
}

/// @inheritdoc BuildScript
/// @dev In declaration order — the order the aggregate emits its entries
/// in.
function snapshotContractNames() internal pure override returns (string[] memory) {
GeneratedContract[] memory contracts = generatedContracts();
string[] memory names = new string[](contracts.length);
for (uint256 i = 0; i < contracts.length; i++) {
names[i] = contracts[i].contractName;
}
return names;
}

/// @inheritdoc BuildScript
/// @dev Every alias lib, every released-suites lib and the aggregate over
/// them.
function regenerateLibs() internal override {
GeneratedContract[] memory contracts = generatedContracts();
for (uint256 i = 0; i < contracts.length; i++) {
LibRainDeploySnapshot.writeAliasLib(
vm,
LibRainDeploySnapshot.LIB_DIR,
contracts[i].contractName,
contracts[i].constantPrefix,
LibRainDeploySnapshot.CANDIDATE
);
LibRainDeploySnapshot.writeReleasedSuitesLib(
vm,
LibRainDeploySnapshot.LIB_DIR,
recordRoot(),
contracts[i].contractName,
contracts[i].candidate.snapshot
);
}
LibRainDeploySnapshot.writeReleasedSuitesAggregate(vm, LibRainDeploySnapshot.LIB_DIR, snapshotContractNames());
}

/// @inheritdoc BuildScript
function regenerateSnapshots() internal override {
GeneratedContract[] memory contracts = generatedContracts();
for (uint256 i = 0; i < contracts.length; i++) {
LibRainDeploySnapshot.writeSnapshot(
vm,
LibRainDeploySnapshot.CANDIDATE,
contracts[i].contractName,
contracts[i].candidate.sourceCreationCode,
contracts[i].candidate.snapshot.dependencies
);
}
}
}
Loading
Loading