Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
60 changes: 0 additions & 60 deletions .github/workflows/manual-sol-artifacts.yaml

This file was deleted.

54 changes: 31 additions & 23 deletions .github/workflows/package-release.yaml
Original file line number Diff line number Diff line change
@@ -1,37 +1,45 @@
name: Package Release
on:
push:
branches:
- main
tags:
- sol-v*
jobs:
release:
# rainix-autopublish's job requests contents: write (to push the version-
# bump commit + tag) and id-token: write. This repo's default workflow-token
# permission is read-only, so without granting these here the reusable's
# request exceeds the caller and the run fails at startup. Grant exactly the
# scopes the reusable needs, per workflow, rather than flipping the whole
# repo default to write.
# rainix-tag-release verifies the tag, publishes to soldeer, freezes the
# candidate snapshot into a numbered release dir, and commits it back — so it
# needs contents: write (push the release commit). This repo's default
# workflow-token permission is read-only, so grant that scope here or the
# reusable's request exceeds the caller and the run fails at startup. No
# id-token: nothing in the reusable uses OIDC (Soldeer publishes via
# SOLDEER_API_TOKEN).
permissions:
contents: write
id-token: write
uses: rainlanguage/rainix/.github/workflows/rainix-autopublish.yaml@main
# A DEPLOY repo is on the tag-release lifecycle, not autopublish. A release
# is cut by pushing a `sol-vX.Y.Z` tag (never on merge); deployment is a
# separate manual step. `[package].version` is the LAST released version and
# moves only at release, in lockstep with the frozen snapshot the tag cuts.
# This is the counterpart to library repos, which use rainix-autopublish and
# bump a next-version slot on every merge. Mirrors the rain.factory.deploy
# caller. `cut-release.sh` freezes `src/generated/candidate/` into
# `src/generated/<version>/` (see the script) then regenerates the libs.
uses: rainlanguage/rainix/.github/workflows/rainix-tag-release.yaml@main
with:
# `foundry.toml` version is the NEXT (in-development) release (see #244). On
# merge, publish that version and bump to the next — the bump no longer
# generates artifacts (rainix#267). A version's deploy-pin snapshot is built
# by the PR that changes the bytecode: run
# `forge script ./script/BuildPointers.sol` (after `forge soldeer install`)
# and commit the new `src/generated/<tag>/` snapshot + regenerated deploy
# libs. The LibProdDeploy fork/deploy tests fail if a snapshot is stale, and
# the rainix append-only check (rainix#268) rejects any change to a snapshot
# already on main, so snapshots stay frozen once merged.
soldeer-package: st0x-deploy
# Secrets are passed explicitly (not `inherit`), mirroring this repo's other
# cross-org rainix callers (e.g. rainix-sol.yaml). Only SOLDEER_API_TOKEN is
# required for the publish; the rest are optional and fall back cleanly when
# empty (bot git identity; GITHUB_TOKEN push, since main is unprotected).
tag-prefix: sol-v
snapshot-generate-cmd: bash script/cut-release.sh
# Secrets passed explicitly (not `inherit`), mirroring this repo's other
# cross-org rainix callers — exactly the set rainix-tag-release declares. The
# RPC_URL_*_FORK secrets are what its verify step forks with; omitting them
# reintroduces the "env var not found" verify failure.
secrets:
SOLDEER_API_TOKEN: ${{ secrets.SOLDEER_API_TOKEN }}
PUBLISH_PRIVATE_KEY: ${{ secrets.PUBLISH_PRIVATE_KEY }}
CI_GIT_EMAIL: ${{ secrets.CI_GIT_EMAIL }}
CI_GIT_USER: ${{ secrets.CI_GIT_USER }}
RPC_URL_ARBITRUM_FORK: ${{ secrets.RPC_URL_ARBITRUM_FORK }}
RPC_URL_BASE_FORK: ${{ secrets.RPC_URL_BASE_FORK }}
RPC_URL_BASE_SEPOLIA_FORK: ${{ secrets.RPC_URL_BASE_SEPOLIA_FORK }}
RPC_URL_ETHEREUM_FORK: ${{ secrets.RPC_URL_ETHEREUM_FORK }}
RPC_URL_FLARE_FORK: ${{ secrets.RPC_URL_FLARE_FORK }}
RPC_URL_POLYGON_FORK: ${{ secrets.RPC_URL_POLYGON_FORK }}
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
52 changes: 22 additions & 30 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,36 +283,28 @@ library. When making changes to contract source:

## Deployment

`script/Deploy.sol` dispatches based on `DEPLOYMENT_SUITE` env var. One contract
per suite to avoid Zoltu factory nonce issues. Every suite targets the
`LibProdDeployV4` (rain.vats 0.1.6) pins and broadcasts to every network in
`LibStoxDeployNetworks.supportedNetworks()` (Base + Ethereum mainnet) — the
Zoltu deploy is idempotent per network, so a suite run skips networks that
already carry the artifact and deploys the identical bytecode to the ones that
don't. The frozen V1/V2 deployments in `LibProdDeployV1` / `LibProdDeployV2` are
an audit trail and are not redeployable from the current source.

- `stox-receipt-v4` — deploys StoxReceipt
- `stox-receipt-vault-v4` — deploys StoxReceiptVault
- `stox-wrapped-token-vault-v4` — deploys StoxWrappedTokenVault
- `stox-wrapped-token-vault-beacon-v4` — deploys StoxWrappedTokenVaultBeacon
(depends on StoxWrappedTokenVault)
- `stox-wrapped-token-vault-beacon-set-deployer-v4` — deploys
StoxWrappedTokenVaultBeaconSetDeployer (depends on beacon)
- `stox-offchain-asset-receipt-vault-beacon-set-deployer-v4` — deploys
StoxOffchainAssetReceiptVaultBeaconSetDeployer (depends on StoxReceipt,
StoxReceiptVault)
- `stox-unified-deployer-v4` — deploys StoxUnifiedDeployer (depends on both
beacon-set deployers)
- `stox-offchain-asset-receipt-vault-authorizer-v1-v4` — deploys
StoxOffchainAssetReceiptVaultAuthorizerV1
- `stox-offchain-asset-receipt-vault-payment-mint-authorizer-v1-v4` — deploys
StoxOffchainAssetReceiptVaultPaymentMintAuthorizerV1
- `stox-corporate-actions-facet-v4` — deploys StoxCorporateActionsFacet

Manual deployment runs via the GitHub Actions workflow
(`manual-sol-artifacts.yaml`), which broadcasts each suite to every supported
network in one run.
Production deploys run the audited, version-specific script
`script/DeployProdV4_0_1_1.sol` — never the current source. It ships the stored
audited-0.1.1 creation bytecode (`LibProdDeployV4.*_CREATION_CODE_0_1_1`, the
exact bytes the 0.1.1 audit covers) and asserts each contract against its
`_0_1_1` address/codehash pin, so the on-chain result is the audited deployment
regardless of what the current source compiles to. Each dispatch deploys one
contract suite (`DEPLOYMENT_SUITE`, one contract per suite to avoid Zoltu
factory nonce issues) to a single bootstrap network (`DEPLOYMENT_NETWORK` —
Ethereum or HyperEVM), via the `manual-sol-artifacts-0-1-1.yaml` GitHub Actions
workflow; the Zoltu deploy is idempotent per network. Suites are deployed in the
workflow's listed order — later suites reference earlier ones via dependency
pointers, so an out-of-order run trips the dep-codehash check. The orchestrator
(introduced at 0.1.2) is not part of the audited 0.1.1 set.

The rolling `candidate` snapshot (`src/generated/candidate/`) is NEVER a deploy
target — it exists only so tests exercise the current source and
`testCandidateSelfConsistent` pins source↔snapshot integrity. When a later
version is audited it gets its own frozen numbered snapshot (cut from
`candidate` by a `sol-vX.Y.Z` release tag; see `script/cut-release.sh`) and its
own `script/DeployProdV4_0_1_N.sol`. The frozen V1/V2 deployments in
`LibProdDeployV1` / `LibProdDeployV2` are an audit trail and are not
redeployable from the current source.

## Naming Conventions

Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# clean. So this holds the next unpublished version, not the last published one.
[package]
name = "st0x-deploy"
version = "0.1.30"
version = "0.1.1"

[profile.default]
libs = ['dependencies']
Expand Down
2 changes: 1 addition & 1 deletion script/20260619-deploy-v4-authoriser-clone.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ error GrantsSliceOutOfRange(uint256 startIndex, uint256 sliceLength, uint256 gra
/// key executes them in sequence in one `forge script --broadcast`
/// invocation. Dispatched via `.github/workflows/manual-broadcast.yaml`,
/// which broadcasts as `secrets.PRIVATE_KEY` — the same CI-held deploy
/// key `manual-sol-artifacts.yaml` uses for Zoltu impl deploys. The Safe
/// key `manual-sol-artifacts-0-1-1.yaml` uses for Zoltu impl deploys. The Safe
/// never signs anything for this deploy: the whole clone-configuration
/// ceremony collapses into a workflow-dispatch broadcast matching the
/// impl-deploy pattern the ops flow already uses.
Expand Down
69 changes: 44 additions & 25 deletions script/BuildPointers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,26 @@ import {ST0xOrchestrator} from "../src/concrete/ST0xOrchestrator.sol";
import {ST0xOrchestratorBeaconSetDeployer} from "../src/concrete/deploy/ST0xOrchestratorBeaconSetDeployer.sol";

contract BuildPointers is Script {
/// @notice The canonical release tag. Read from `foundry.toml`
/// `[package].version` — the single source of truth — with dots converted
/// to underscores for the Solidity constant/dir form (`0.1.3` -> `0_1_3`).
/// Everything version-dependent (the `<tag>/` snapshot dir, `DEPLOY_TAG`,
/// the current-release aliases) derives from this.
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; a numbered snapshot is frozen only when
/// a release tag promotes `candidate` (see `script/cut-release.sh`). Source
/// self-references (`LibProdDeployCurrent`) always resolve to `candidate`,
/// so they track whatever the source currently compiles to, while numbered
/// snapshots (`0_1_1`, …) stay frozen and are never regenerated here.
string constant CANDIDATE_TAG = "candidate";

function deployTag() internal pure returns (string memory) {
return CANDIDATE_TAG;
}

/// @notice The constant-name suffix for a tag dir. Numbered tags use the tag
/// verbatim (`0_1_1`); the rolling `candidate` dir uses `CANDIDATE`, so its
/// generated constants read `STOX_RECEIPT_CANDIDATE` rather than the
/// lowercase dir name.
function tagSuffix(string memory tag) internal pure returns (string memory) {
if (keccak256(bytes(tag)) == keccak256(bytes(CANDIDATE_TAG))) return "CANDIDATE";
return tag;
}

function addressConstantString(address addr) internal pure returns (string memory) {
Expand Down Expand Up @@ -88,8 +95,8 @@ contract BuildPointers is Script {
function run() external {
LibRainDeploy.etchZoltuFactory(vm);

// A fresh next-version slot has no `<tag>/` dir yet, and `vm.writeFile`
// won't create one.
// Regenerate the rolling `candidate/` snapshot from current source.
// `vm.writeFile` won't create the dir, so ensure it exists first.
vm.createDir(string.concat("src/generated/", deployTag()), true);

buildContractPointers("StoxCorporateActionsFacet", type(StoxCorporateActionsFacet).creationCode);
Expand Down Expand Up @@ -215,7 +222,10 @@ contract BuildPointers is Script {
}

/// @notice A monotonic sort key for an `a_b_c` tag (each component < 1e6).
/// The non-numeric `candidate` tag sorts last (the rolling head after every
/// frozen numbered release).
function tagKey(string memory name) internal pure returns (uint256 key) {
if (keccak256(bytes(name)) == keccak256(bytes(CANDIDATE_TAG))) return type(uint256).max;
bytes memory b = bytes(name);
uint256 num = 0;
for (uint256 i = 0; i < b.length; i++) {
Expand All @@ -239,7 +249,7 @@ contract BuildPointers is Script {
for (uint256 i = 0; i < entries.length; i++) {
if (!entries[i].isDir) continue;
string memory name = baseName(entries[i].path);
if (isTagName(name)) {
if (isTagName(name) || keccak256(bytes(name)) == keccak256(bytes(CANDIDATE_TAG))) {
tmp[n] = name;
n++;
}
Expand Down Expand Up @@ -278,31 +288,36 @@ contract BuildPointers is Script {
pure
returns (string memory)
{
string memory suffix = tagSuffix(tag);
string memory head =
string.concat("import {DEPLOYED_ADDRESS as ", base, "_ADDRESS_", tag, "_GEN, BYTECODE_HASH as ", base);
string.concat("import {DEPLOYED_ADDRESS as ", base, "_ADDRESS_", suffix, "_GEN, BYTECODE_HASH as ", base);
string memory mid = string.concat(
"_CODEHASH_", tag, "_GEN, CREATION_CODE as ", base, "_CREATION_", tag, "_GEN, RUNTIME_CODE as ", base
"_CODEHASH_", suffix, "_GEN, CREATION_CODE as ", base, "_CREATION_", suffix, "_GEN, RUNTIME_CODE as ", base
);
string memory tail = string.concat("_RUNTIME_", tag, '_GEN} from "./', tag, "/", name, '.pointers.sol";');
string memory tail = string.concat("_RUNTIME_", suffix, '_GEN} from "./', tag, "/", name, '.pointers.sol";');
return string.concat(head, mid, tail);
}

/// @notice Emit the four aliased constants for one (tag, contract).
function emitV4Constants(string memory tag, string memory base) internal {
string memory suffix = tagSuffix(tag);
vm.writeLine(
GEN_V4_PATH, string.concat("address constant ", base, "_", tag, " = ", base, "_ADDRESS_", tag, "_GEN;")
GEN_V4_PATH,
string.concat("address constant ", base, "_", suffix, " = ", base, "_ADDRESS_", suffix, "_GEN;")
);
vm.writeLine(
GEN_V4_PATH,
string.concat("bytes32 constant ", base, "_CODEHASH_", tag, " = ", base, "_CODEHASH_", tag, "_GEN;")
string.concat("bytes32 constant ", base, "_CODEHASH_", suffix, " = ", base, "_CODEHASH_", suffix, "_GEN;")
);
vm.writeLine(
GEN_V4_PATH,
string.concat("bytes constant ", base, "_CREATION_CODE_", tag, " = ", base, "_CREATION_", tag, "_GEN;")
string.concat(
"bytes constant ", base, "_CREATION_CODE_", suffix, " = ", base, "_CREATION_", suffix, "_GEN;"
)
);
vm.writeLine(
GEN_V4_PATH,
string.concat("bytes constant ", base, "_RUNTIME_CODE_", tag, " = ", base, "_RUNTIME_", tag, "_GEN;")
string.concat("bytes constant ", base, "_RUNTIME_CODE_", suffix, " = ", base, "_RUNTIME_", suffix, "_GEN;")
);
}

Expand Down Expand Up @@ -357,6 +372,7 @@ contract BuildPointers is Script {
/// current release tag.
function genCurrent() internal {
string memory tag = deployTag();
string memory suffix = tagSuffix(tag);
require(vm.exists(string.concat("src/generated/", tag)), "BuildPointers: current tag dir missing");
string[12] memory names = contractNames();
string[12] memory bases = contractBases();
Expand All @@ -379,11 +395,14 @@ contract BuildPointers is Script {
if (!pointerExists(tag, names[c])) continue;
string memory base = bases[c];
vm.writeLine(
GEN_CURRENT_PATH, string.concat("address constant ", base, " = LibProdDeployV4.", base, "_", tag, ";")
GEN_CURRENT_PATH,
string.concat("address constant ", base, " = LibProdDeployV4.", base, "_", suffix, ";")
);
vm.writeLine(
GEN_CURRENT_PATH,
string.concat("bytes32 constant ", base, "_CODEHASH = LibProdDeployV4.", base, "_CODEHASH_", tag, ";")
string.concat(
"bytes32 constant ", base, "_CODEHASH = LibProdDeployV4.", base, "_CODEHASH_", suffix, ";"
)
);
}
vm.writeLine(GEN_CURRENT_PATH, "}");
Expand Down
Loading
Loading