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
21 changes: 16 additions & 5 deletions .github/workflows/manual-sol-artifacts.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
name: Manual sol artifacts
# The on-chain deploy, human-dispatched, run BEFORE tagging a release. Two
# suites, matching `script/Deploy.sol`'s DEPLOYMENT_SUITE switch: deploy the
# log-tables data contract once per chain, then the DecimalFloat concrete (its
# constructor reads the log tables at the Zoltu address). Never broadcasts on
# merge.
# The on-chain deploy, human-dispatched, run BEFORE tagging a release. Deploy
# the log-tables data contract once per chain, then the DecimalFloat concrete
# (its constructor reads the log tables at the Zoltu address). Never broadcasts
# on merge.
#
# The `suite` input below is passed as DEPLOYMENT_SUITE. `script/Deploy.sol`
# holds no switch on it — it is a declaration plus `RainDeployBroadcast`, and
# upstream's `RainDeployBroadcast.run()` resolves the value through
# `suiteByName` against the keys `src/abstract/DecimalFloatDeploySuites.sol`
# declares. A mistyped key reports the valid ones from there.
#
# Those keys are hand-listed as `options` because a workflow input cannot read
# them out of Solidity. They are the one place in this repo that restates the
# declaration, so a suite added there needs a line added here; an unlisted suite
# simply cannot be dispatched, which fails visibly rather than deploying the
# wrong thing.
on:
workflow_dispatch:
inputs:
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ generated deploy records and the deploy scripts + tests. The pure math
frozen `src/generated/<tag>/` snapshot; a normal PR never bumps it.
- `DecimalFloat`'s constructor reverts unless the log tables are at their Zoltu
address: tables deploy/build/broadcast FIRST, always.
- `src/generated/LogTables.pointers.sol` is table BYTES (a pure function of
- `src/generated/LogTables.bytes.sol` is table BYTES (a pure function of
`LibLogTable`), not a deploy record — it stays at the generated root, never in
a tag dir.

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ concrete `DecimalFloat` contract, the rolling `src/generated/candidate/`
snapshots of its deterministic Zoltu deploy records (address, codehash, creation
and runtime bytecode), the alias lib `src/lib/deploy/LibDecimalFloatDeploy.sol`
over those pins, the generated log-tables bytes
(`src/generated/LogTables.pointers.sol`), and the deploy scripts + tests.
(`src/generated/LogTables.bytes.sol`), and the deploy scripts + tests.

The **library** half — `LibDecimalFloat`, `LibDecimalFloatImplementation`,
`LibFormatDecimalFloat`, `LibParseDecimalFloat`, `LibLogTable` and the errors —
Expand All @@ -33,7 +33,7 @@ consumers that need the deployed address, codehash or the deploy pins
`checkLogTablesDeployed()`.
- `src/lib/LibReleasedSuites.sol` (+ the per-contract `Lib*Released.sol`) — the
generated record of every released suite. NEVER edit by hand.
- `src/generated/LogTables.pointers.sol` — the AOT-compiled log/anti-log table
- `src/generated/LogTables.bytes.sol` — the AOT-compiled log/anti-log table
bytes, regenerated (not hand-written) by `script/Build.sol`. The bytes are a
pure function of `LibLogTable`, so this snapshot is version-invariant. NEVER
edit by hand.
Expand Down
30 changes: 18 additions & 12 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ libs = ["dependencies"]

# Write access is scoped to exactly the files `script/Build.sol` generates:
# everything under `src/generated/` (the candidate snapshots, the frozen tag
# dirs `cutRelease()` adds, `LogTables.pointers.sol`) and the three generated
# dirs `cutRelease()` adds, `LogTables.bytes.sol`) and the three generated
# released-suites libs in `src/lib/` — never hand-written source. It reads the
# release version from this file when `cutRelease()` runs, and
# `DecimalFloatDeploySnapshotTest`'s inherited frozen-record walk reads
Expand Down Expand Up @@ -86,7 +86,7 @@ forge-std = "1.16.2"
recursive_deps = false

# One alias per network `script/Deploy.sol` broadcasts to, which is
# `LibRainDeploy.supportedNetworks()` — all seven. A network the script
# `LibRainDeploy.supportedNetworks()`. A network the script
# selects but this section does not name is not a skip: `vm.createSelectFork`
# reverts with "invalid rpc url: <alias>" and takes the whole deploy down,
# after it has already broadcast to the networks ahead of it in the list.
Expand All @@ -106,17 +106,23 @@ polygon = "${POLYGON_RPC_URL}"
# 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 ethereum and hyperevm because foundry does not know
# those alias strings as chain names — its own names for them are "mainnet"
# and "hyperliquid" — so without it the entry cannot be matched to the chain
# being verified against. The other five aliases foundry resolves itself.
# `forge config` prints the resolved chain per entry.
# "chain" is stated on EVERY entry, not only the aliases foundry cannot resolve
# itself. `rain-deploy`'s `checkEtherscanEntriesResolvable` requires it, because
# the set foundry resolves is foundry's own table and moves under a toolchain
# bump — and foundry resolves the whole section rather than the single entry a
# `--verify` needs, so one unresolvable entry fails the verify for every network.
# Stating a chain an alias already resolves to resolves it to the same chain, so
# the strict form only ever agrees with the loose one.
#
# The ids are verified rather than trusted: `testSupportedNetworkChainIdsAreBound`
# forks each network and compares. `forge config` prints the resolved chain per
# entry.
[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}" }
arbitrum = { key = "${CI_DEPLOY_ARBITRUM_ETHERSCAN_API_KEY}", chain = 42161 }
base = { key = "${CI_DEPLOY_BASE_ETHERSCAN_API_KEY}", chain = 8453 }
base_sepolia = { key = "${CI_DEPLOY_BASE_SEPOLIA_ETHERSCAN_API_KEY}", chain = 84532 }
ethereum = { key = "${CI_DEPLOY_ETHEREUM_ETHERSCAN_API_KEY}", chain = 1 }
flare = { key = "${CI_DEPLOY_FLARE_ETHERSCAN_API_KEY}" }
flare = { key = "${CI_DEPLOY_FLARE_ETHERSCAN_API_KEY}", chain = 14 }
hyperevm = { key = "${CI_DEPLOY_HYPEREVM_ETHERSCAN_API_KEY}", chain = 999 }
# Robinhood Chain (4663) is not indexed by Etherscan V2. Its Blockscout speaks
# the Etherscan API, so the entry points there (the key is ignored). That
Expand All @@ -125,4 +131,4 @@ hyperevm = { key = "${CI_DEPLOY_HYPEREVM_ETHERSCAN_API_KEY}", chain = 999 }
# sourcify --chain 4663 ...`.
robinhood = { key = "${CI_DEPLOY_ROBINHOOD_ETHERSCAN_API_KEY}", chain = 4663, url = "https://robinhoodchain.blockscout.com/api" }
bsc = { key = "${CI_DEPLOY_BSC_ETHERSCAN_API_KEY}", chain = 56 }
polygon = { key = "${CI_DEPLOY_POLYGON_ETHERSCAN_API_KEY}" }
polygon = { key = "${CI_DEPLOY_POLYGON_ETHERSCAN_API_KEY}", chain = 137 }
31 changes: 22 additions & 9 deletions script/Build.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,34 @@ import {LibRainDeploySnapshot} from "rain-deploy-0.1.11/src/lib/LibRainDeploySna
import {DeployCandidate} from "../src/abstract/RainDeploySuitesBase.sol";
import {DecimalFloatDeploySuites} from "../src/abstract/DecimalFloatDeploySuites.sol";

/// @dev Committed path of the generated log tables. The `.pointers.sol` suffix
/// is the one the deploy pins and importers reference. The file is pure
/// log-table data with no contract instance behind it, so it carries no
/// bytecode-hash constant and is written directly rather than through
/// @dev Committed LEAF NAME of the generated log tables, under `recordRoot()`.
///
/// The directory is not spelled here. Every other write in this script takes it
/// from `recordRoot()`, and spelling `src/generated` again would make this the
/// one path upstream does not own: it would keep writing to the old place if
/// `LIB_FS_ROOT` moved, and would ignore a `recordRoot()` override, which exists
/// so `cutRelease()` can be exercised against a record other than the repo's own.
///
/// The leaf is what importers reference, so changing it is a downstream edit.
/// It was `LogTables.pointers.sol` until this branch, which named the file after
/// a payload it does not hold: there are no pointers in it, only five `bytes`
/// constants of table data. The name came from the rainlang convention where
/// `LibCodeGen.bytesConstantString` output IS function pointers, and followed
/// the generator's usual payload rather than this one's.
///
/// The file is pure log-table data with no contract instance behind it, so it
/// carries no bytecode-hash constant and is written directly rather than through
/// `LibFs.buildFileForContract`, which heads every file it writes with the hash
/// of an instance and reverts on the codeless `address(0)` this build has.
///
/// It sits at the root of `src/generated/` rather than in a snapshot directory
/// It sits at the root of the record rather than in a snapshot directory
/// because it is not a deploy record: it holds the table BYTES, which are a
/// pure function of `LibLogTable` and carry no address, code hash or tag. The
/// deploy record of the data contract those bytes are wrapped in is
/// `src/generated/candidate/LogTables.sol`, and only tag-shaped directories
/// under this root are releases — a loose file here is one neither
/// `frozenSnapshotPaths` nor `release-guard` reads as a snapshot.
string constant GENERATED_LOG_TABLES = "src/generated/LogTables.pointers.sol";
string constant GENERATED_LOG_TABLES_LEAF = "LogTables.bytes.sol";

/// One contract's generated files: the rolling snapshot and the released-suites
/// lib emitted from its record.
Expand Down Expand Up @@ -75,7 +88,7 @@ contract Build is BuildScript, DecimalFloatDeploySuites {
return names;
}

/// Rewrites `src/generated/LogTables.pointers.sol` from `LibLogTable`.
/// Rewrites `src/generated/LogTables.bytes.sol` from `LibLogTable`.
///
/// The bytes are a pure function of that library's mathematics, so this
/// file is a single snapshot rather than a per-tag directory: there is no
Expand All @@ -84,7 +97,7 @@ contract Build is BuildScript, DecimalFloatDeploySuites {
function regenerateLogTables() internal {
//forge-lint: disable-next-line(unsafe-cheatcode)
vm.writeFile(
GENERATED_LOG_TABLES,
string.concat(recordRoot(), "/", GENERATED_LOG_TABLES_LEAF),
string.concat(
LibCodeGen.filePrefix(),
LibCodeGen.bytesConstantString(
Expand Down Expand Up @@ -126,7 +139,7 @@ contract Build is BuildScript, DecimalFloatDeploySuites {
/// a freeze copies whatever this hook leaves behind, so regenerating them
/// anywhere but here would let a release freeze a snapshot cut from stale
/// tables. They are written from `LibLogTable` while the snapshot below
/// is cut from the COMPILED-IN `LogTables.pointers.sol`, so a run that
/// is cut from the COMPILED-IN `LogTables.bytes.sol`, so a run that
/// actually moves the tables converges on the second run — which is what
/// the currency check in CI reports rather than hides.
/// - `writeSnapshot` runs each creation code through the Zoltu factory to
Expand Down
43 changes: 33 additions & 10 deletions script/lib/LibEtchLogTables.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,48 @@ pragma solidity ^0.8.25;
import {Vm} from "forge-std-1.16.2/src/Vm.sol";
import {LibDataContract} from "rain-datacontract-0.1.9/src/lib/LibDataContract.sol";
import {LibDecimalFloatDeploy} from "src/lib/deploy/LibDecimalFloatDeploy.sol";
import {LibRainDeploy} from "rain-deploy-0.1.11/src/lib/LibRainDeploy.sol";

/// @notice Shared logic for planting the log-tables data contract at its
/// Zoltu-deterministic address inside a forge VM. Used by `script/Deploy.sol`
/// (so the decimal-float simulation pass can pass the constructor's codehash
/// check before log-tables exists on-chain) and by tests that need
/// Zoltu-deterministic address inside a forge VM, for tests that need
/// `DecimalFloat` operations to work without a real on-chain deploy.
///
/// NOT used by `script/Deploy.sol`, which does not import this and must not:
/// its docstring states that nothing plants the tables locally to get a
/// simulation through, because a chain without them is a chain the suite must
/// not be deployed to — `DecimalFloat`'s constructor reverts there, and
/// `LibRainDeploy.deployToNetworks` raises `MissingDependency` rather than
/// papering over it. An earlier version of this comment claimed the broadcast
/// as a consumer; the broadcast is entirely `RainDeployBroadcast.run()` and has
/// no etch hook to call this from.
///
/// Callers are `test/abstract/LogTest.sol`,
/// `test/src/concrete/DecimalFloat.constructor.t.sol`,
/// `test/src/abstract/DecimalFloatDeployChain.t.sol` and
/// `test/src/abstract/DecimalFloatDeploySnapshot.t.sol`.
library LibEtchLogTables {
/// @notice Deploys the log-tables data contract to a temporary address,
/// copies its runtime code, and etches that runtime at
/// `LibDecimalFloatDeploy.ZOLTU_DEPLOYED_LOG_TABLES_ADDRESS` so the
/// codehash matches `LibDecimalFloatDeploy.LOG_TABLES_DATA_CONTRACT_HASH`.
/// Deployed through the Zoltu factory rather than a raw `create` into a
/// temporary address followed by an etch across. The factory is what puts
/// the contract at `ZOLTU_DEPLOYED_LOG_TABLES_ADDRESS` in the first place,
/// so using it makes the address a consequence of the creation code instead
/// of something asserted after the fact — and the temp-address dance existed
/// only because a raw `create` lands at a nonce-dependent address.
///
/// The returned address is checked against the pin. A deployment that does
/// not land on it means the creation code and the committed snapshot have
/// diverged, which is worth a failure here rather than an etch that hides it
/// by writing the runtime wherever the pin says regardless.
function etchLogTables(Vm vm) internal {
LibRainDeploy.etchZoltuFactory(vm);
bytes memory tables = LibDecimalFloatDeploy.combinedTables();
bytes memory creationCode = LibDataContract.contractCreationCode(tables);
address temp;
assembly ("memory-safe") {
temp := create(0, add(creationCode, 0x20), mload(creationCode))
}
require(temp != address(0), "log tables etch deploy failed");
vm.etch(LibDecimalFloatDeploy.ZOLTU_DEPLOYED_LOG_TABLES_ADDRESS, temp.code);
address deployed = LibRainDeploy.deployZoltu(LibDataContract.contractCreationCode(tables));
require(
deployed == LibDecimalFloatDeploy.ZOLTU_DEPLOYED_LOG_TABLES_ADDRESS,
"log tables deployed off their pinned Zoltu address"
);
}
}
2 changes: 1 addition & 1 deletion src/abstract/DecimalFloatDeploySuites.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ abstract contract DecimalFloatDeploySuites is RainDeploySuitesBase {
/// There is no Solidity contract behind it — the creation code is
/// `LibDataContract`'s data-contract wrapper around the bytes
/// `LibDecimalFloatDeploy.combinedTables()` concatenates out of
/// `src/generated/LogTables.pointers.sol` — so `sourceCreationCode` is that
/// `src/generated/LogTables.bytes.sol` — so `sourceCreationCode` is that
/// same pure expression rather than a `type(X).creationCode`, and the
/// artifact path is empty because there is no source file for an explorer
/// to verify against.
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion src/lib/deploy/LibDecimalFloatDeploy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
LOG_TABLES_SMALL_ALT,
ANTI_LOG_TABLES,
ANTI_LOG_TABLES_SMALL
} from "../../generated/LogTables.pointers.sol";
} from "../../generated/LogTables.bytes.sol";
import {
DEPLOYED_ADDRESS as LOG_TABLES_CANDIDATE_ADDRESS,
BYTECODE_HASH as LOG_TABLES_CANDIDATE_HASH
Expand Down
12 changes: 12 additions & 0 deletions test/abstract/LogTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ abstract contract LogTest is Test {
);
}

/// A SECOND copy of the tables, at a nonce-dependent address that is
/// deliberately not `ZOLTU_DEPLOYED_LOG_TABLES_ADDRESS` — `setUp` has
/// already put a copy there.
///
/// The raw `create` is the point and not an un-migrated call site. Tests
/// that take a log-tables address as an argument use this one so that
/// passing it is distinguishable from reading the pinned constant: if both
/// addresses were the pin, a function that ignored its argument entirely
/// would still pass. `LibRainDeploy.deployZoltu` cannot serve here, because
/// the address it lands on is a function of the creation code alone, so for
/// this creation code it is the pin — which `setUp` has occupied, making the
/// second deployment fail rather than yield a distinct address.
function logTables() internal returns (address) {
if (sTables == address(0)) {
bytes memory tables = LibDecimalFloatDeploy.combinedTables();
Expand Down
25 changes: 20 additions & 5 deletions test/src/abstract/DecimalFloatDeployChain.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,34 @@
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity =0.8.25;

import {RainDeployVerifyChain} from "rain-deploy-0.1.11/src/abstract/RainDeployVerifyChain.sol";
import {RainDeployVerify} from "rain-deploy-0.1.11/src/abstract/RainDeployVerify.sol";
import {DecimalFloatDeploySuites} from "src/abstract/DecimalFloatDeploySuites.sol";
import {LibEtchLogTables} from "script/lib/LibEtchLogTables.sol";

/// @title DecimalFloatDeployChainTest
/// @notice Binds this repo's declaration to `RainDeployVerifyChain`: every
/// frozen release of the log tables and of `DecimalFloat` is live, with the
/// code it froze, on every supported network.
/// @notice Binds this repo's declaration to `RainDeployVerify`: every frozen
/// release of the log tables and of `DecimalFloat` is live, with the code it
/// froze, on every supported network.
///
/// The live CURRENT pins are checked by `LibDecimalFloatDeployProdTest`, which
/// is a different claim: the candidate is what the NEXT release will be, and a
/// released suite is a deployment that already happened.
contract DecimalFloatDeployChainTest is DecimalFloatDeploySuites, RainDeployVerifyChain {
///
/// `RainDeployVerify` rather than `RainDeployVerifyChain`, because upstream
/// states it is "the ONE contract a deploy repo binds" and reserves the right to
/// put a new check directly on it: a check on the union reaches a repo that
/// bound only the two halves never, and nothing red-lines the repo that misses
/// it. Binding the union here is what makes a version bump deliver it.
///
/// `DecimalFloatDeploySnapshotTest` still binds `RainDeployVerifySnapshot`
/// alone, which is why that half is bound twice. That is deliberate and is the
/// cheap side of the trade: the snapshot group forks nothing, so binding it in
/// its own contract is what lets a job with no RPC credentials select it with
/// `--match-contract` — selection happens at a contract boundary, so a single
/// combined contract could not offer it. The duplicated runs are network-free
/// and fast; losing the credential-free run, or losing a future union-level
/// check, would both cost more.
contract DecimalFloatDeployChainTest is DecimalFloatDeploySuites, RainDeployVerify {
/// Plants the log tables for the same reason the snapshot suite does: the
/// derivations all run on the local EVM before anything forks, and
/// `DecimalFloat`'s constructor reverts without the tables in place.
Expand Down
Loading
Loading