diff --git a/CLAUDE.md b/CLAUDE.md index 4c277ed..bae88d9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -70,7 +70,8 @@ as the `rain-factory` Soldeer dependency, so they are read under 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//` snapshot). `DEPLOY_TAG` names that snapshot, so which + release the pins came from is a readable constant rather than an import path. - `src/generated//CloneFactory.pointers.sol` — Frozen per-release deploy-pin snapshots: creation code, runtime code, bytecode hash, deployed address. diff --git a/script/BuildPointers.sol b/script/BuildPointers.sol index e108599..32c8da5 100644 --- a/script/BuildPointers.sol +++ b/script/BuildPointers.sol @@ -88,10 +88,14 @@ contract BuildPointers is Script { /// @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 - /// generated-file convention. + /// truth (never a duplicated literal). Also emits that tag as `DEPLOY_TAG`, + /// so which snapshot the pins came from is a readable constant rather than + /// an import path, and `LibCloneFactoryDeployTagTest` can assert it against + /// `[package].version`. Emitted line-by-line to match the generated-file + /// convention. function genLibCloneFactoryDeploy() internal { - string memory importPath = string.concat("../generated/", deployTag(), "/CloneFactory.pointers.sol"); + string memory tag = deployTag(); + string memory importPath = string.concat("../generated/", tag, "/CloneFactory.pointers.sol"); vm.writeFile(GEN_LIB_PATH, ""); vm.writeLine(GEN_LIB_PATH, GEN_SPDX_LICENSE); vm.writeLine(GEN_LIB_PATH, GEN_SPDX_COPYRIGHT); @@ -111,6 +115,10 @@ contract BuildPointers is Script { 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, "library LibCloneFactoryDeploy {"); + vm.writeLine(GEN_LIB_PATH, " /// @dev The `src/generated//` snapshot the pins below are aliased"); + vm.writeLine(GEN_LIB_PATH, " /// from, i.e. `[package].version` with dots as underscores."); + vm.writeLine(GEN_LIB_PATH, string.concat(' string constant DEPLOY_TAG = "', tag, '";')); + 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/src/lib/LibCloneFactoryDeploy.sol b/src/lib/LibCloneFactoryDeploy.sol index d63d7ef..3072940 100644 --- a/src/lib/LibCloneFactoryDeploy.sol +++ b/src/lib/LibCloneFactoryDeploy.sol @@ -16,6 +16,10 @@ import { /// single source of truth. Lets consumers verify/deploy against a precommitted /// address + hash rather than a registry. library LibCloneFactoryDeploy { + /// @dev The `src/generated//` snapshot the pins below are aliased + /// from, i.e. `[package].version` with dots as underscores. + string constant DEPLOY_TAG = "0_1_5"; + address constant CLONE_FACTORY_DEPLOYED_ADDRESS = CLONE_FACTORY_ADDR; bytes32 constant CLONE_FACTORY_DEPLOYED_CODEHASH = CLONE_FACTORY_HASH; } diff --git a/test/src/lib/LibCloneFactoryDeployTag.t.sol b/test/src/lib/LibCloneFactoryDeployTag.t.sol new file mode 100644 index 0000000..40c36c6 --- /dev/null +++ b/test/src/lib/LibCloneFactoryDeployTag.t.sol @@ -0,0 +1,76 @@ +// 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 {LibCloneFactoryDeploy} from "../../../src/lib/LibCloneFactoryDeploy.sol"; + +/// @title LibCloneFactoryDeployTagTest +/// @notice A release moves three things together: `[package].version` in +/// `foundry.toml`, the frozen `src/generated//` snapshot it names, and the +/// `LibCloneFactoryDeploy` pins aliased out of that snapshot. Nothing else +/// checks they still name the same tag. `LibCloneFactoryDeployTest` catches +/// bytecode drift (current source vs the aliased pins); this catches the other +/// direction — a hand-edited `[package].version` that outruns the snapshot, or +/// a lib left aliasing a superseded one — which would otherwise publish a +/// version whose pins belong to a different release. +contract LibCloneFactoryDeployTagTest is Test { + string constant SNAPSHOT_DIR_PREFIX = "src/generated/"; + string constant SNAPSHOT_FILE_SUFFIX = "/CloneFactory.pointers.sol"; + + /// The canonical `foundry.toml` `[package].version` in `src/generated//` + /// dir form, i.e. dots as underscores (`0.1.5` -> `0_1_5`). Read rather than + /// hardcoded, so this passes across releases without hand-editing. + function packageVersionTag() internal view returns (string memory) { + bytes memory v = bytes(vm.parseTomlString(vm.readFile("foundry.toml"), ".package.version")); + for (uint256 i = 0; i < v.length; i++) { + if (v[i] == ".") v[i] = "_"; + } + return string(v); + } + + function snapshotPath() internal pure returns (string memory) { + return string.concat(SNAPSHOT_DIR_PREFIX, LibCloneFactoryDeploy.DEPLOY_TAG, SNAPSHOT_FILE_SUFFIX); + } + + /// The generated tag MUST equal the canonical `foundry.toml` version. + function testDeployTag() external view { + assertEq(LibCloneFactoryDeploy.DEPLOY_TAG, packageVersionTag()); + } + + /// A frozen snapshot by that name MUST exist — the version can only name a + /// release that was actually snapshotted. + function testDeployTagSnapshotExists() external view { + assertTrue(vm.exists(snapshotPath()), "no src/generated snapshot for DEPLOY_TAG"); + } + + /// The pins the lib exposes MUST be the ones recorded in THAT snapshot, so + /// the alias cannot silently point at another tag. Asserted against the + /// snapshot text because Solidity cannot import a path built at runtime; the + /// literals are rendered exactly as `BuildPointers` writes them. + function testDeployTagSnapshotHoldsTheAliasedPins() external view { + string memory snapshot = vm.readFile(snapshotPath()); + assertTrue( + vm.contains( + snapshot, + string.concat( + "address constant DEPLOYED_ADDRESS = address(", + vm.toString(LibCloneFactoryDeploy.CLONE_FACTORY_DEPLOYED_ADDRESS), + ");" + ) + ), + "DEPLOY_TAG snapshot does not record the aliased deploy address" + ); + assertTrue( + vm.contains( + snapshot, + string.concat( + "bytes32 constant BYTECODE_HASH = bytes32(", + vm.toString(LibCloneFactoryDeploy.CLONE_FACTORY_DEPLOYED_CODEHASH), + ");" + ) + ), + "DEPLOY_TAG snapshot does not record the aliased codehash" + ); + } +}