Skip to content
20 changes: 10 additions & 10 deletions script/MigrateMultisigThreshold.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {Script} from "forge-std-1.16.1/src/Script.sol";
import {console2} from "forge-std-1.16.1/src/console2.sol";

import {IGnosisSafe} from "../src/interface/IGnosisSafe.sol";
import {LibProdSafes} from "../src/lib/LibProdSafes.sol";
import {LibSafeInvariants} from "../src/lib/LibSafeInvariants.sol";
import {LibInvariants} from "../src/lib/LibInvariants.sol";
import {LibSafeOps, SafeTx} from "../src/lib/LibSafeOps.sol";

/// @notice A previously emitted Tx Builder JSON artifact (parsed via
Expand All @@ -28,7 +28,7 @@ error VerifyExpectedSingleTx(uint256 actualCount);
/// @title MigrateMultisigThreshold
/// @notice Forge script that authors the ST0x token-owner Safe's
/// multisig threshold migration (1-of-6 -> 3-of-6 against the post-rotation roster). Performs an
/// exhaustive on-chain pre-flight via `LibSafeInvariants.assertAll`
/// exhaustive on-chain pre-flight via `LibInvariants.assertAll`
/// (proxy codehash, singleton + bytecode, version, modules, guard,
/// fallback handler, uniform vault ownership, expected owner set,
/// expected threshold), simulates the post-state, emits a Safe Tx
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand All @@ -44,7 +44,7 @@ error VerifyExpectedSingleTx(uint256 actualCount);
/// artifact wasn't tampered with between authoring and signing.
///
/// Pre-flight uses the no-arg `assertAll(safe)` overload, which defaults
/// the expected threshold and owner set to the `LibProdSafes`-pinned
/// the expected threshold and owner set to the `LibSafeInvariants`-pinned
/// current truth. Post-state uses the full-args overload to override
/// the threshold with the deliberately-changed `TARGET_THRESHOLD`
/// while keeping the owner set pinned.
Expand Down Expand Up @@ -73,12 +73,12 @@ contract MigrateMultisigThreshold is Script {
/// verification in production and we explicitly simulate via
/// `vm.prank`.
function run() external {
IGnosisSafe safe = IGnosisSafe(LibProdSafes.STOX_TOKEN_OWNER_SAFE);
IGnosisSafe safe = IGnosisSafe(LibSafeInvariants.STOX_TOKEN_OWNER_SAFE);
// Pre-flight: every immutable invariant plus the pinned 6-owner
// roster plus the pinned current threshold. Defaults from
// `LibProdSafes` (no-arg overload). Reverts with the relevant
// `LibSafeInvariants` (no-arg overload). Reverts with the relevant
// typed error from the underlying library on first mismatch.
LibSafeInvariants.assertAll(safe);
LibInvariants.assertAll(safe);

// Build the single-tx bundle: a self-call to `changeThreshold(3)`.
SafeTx memory txn = SafeTx({
Expand All @@ -104,7 +104,7 @@ contract MigrateMultisigThreshold is Script {
// implementation that secretly mutates the owner roster, modules,
// or fallback handler as a side effect.
LibSafeOps.simulateSelfCall(safe, txn.data);
LibSafeInvariants.assertAll(safe, TARGET_THRESHOLD, LibProdSafes.expectedOwners());
LibInvariants.assertAll(safe, TARGET_THRESHOLD, LibSafeInvariants.expectedOwners());

// Emit the Tx Builder JSON artifact and write it under `out/`.
SafeTx[] memory txs = new SafeTx[](1);
Expand Down Expand Up @@ -133,7 +133,7 @@ contract MigrateMultisigThreshold is Script {
// forward migration (`changeThreshold(3)`) rather than the
// reversal. The reversal exists only as a fork-local simulation;
// signers never see it.
LibSafeOps.simulateNPlus1Reversal(safe, LibProdSafes.STOX_TOKEN_OWNER_SAFE_THRESHOLD, TARGET_THRESHOLD);
LibSafeOps.simulateNPlus1Reversal(safe, LibSafeInvariants.STOX_TOKEN_OWNER_SAFE_THRESHOLD, TARGET_THRESHOLD);
console2.log("n+1 reversibility check passed: threshold reverted to", safe.getThreshold());
}

Expand All @@ -143,11 +143,11 @@ contract MigrateMultisigThreshold is Script {
/// integrity before signing.
/// @param jsonPath Filesystem path to the Tx Builder JSON to verify.
function verify(string calldata jsonPath) external view {
IGnosisSafe safe = IGnosisSafe(LibProdSafes.STOX_TOKEN_OWNER_SAFE);
IGnosisSafe safe = IGnosisSafe(LibSafeInvariants.STOX_TOKEN_OWNER_SAFE);
// Same pre-flight bundle as `run()`. If the live state has drifted
// since the artifact was authored, the typed error bubbles before
// we even open the file.
LibSafeInvariants.assertAll(safe);
LibInvariants.assertAll(safe);

(uint256 parsedChainId, address parsedSafe, SafeTx[] memory parsedTxs) = LibSafeOps.parseTxBuilderJson(jsonPath);

Expand Down
14 changes: 14 additions & 0 deletions src/interface/IAuthorisable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: LicenseRef-DCL-1.0
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity ^0.8.25;

/// @title IAuthorisable
/// @notice Minimal authoriser-getter surface exposed by ST0x receipt vaults.
/// Returns `address` rather than reusing the upstream `IAuthorizableV1` so
/// the token-invariant checks carry a narrow surface and not the upstream's
/// richer return type.
interface IAuthorisable {
/// @notice The authoriser contract gating restricted vault operations.
/// @return The authoriser address.
function authorizer() external view returns (address);
}
15 changes: 15 additions & 0 deletions src/interface/IOwnable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: LicenseRef-DCL-1.0
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity ^0.8.25;

/// @title IOwnable
/// @notice Minimal `Ownable`-like surface used by ST0x receipt vaults.
/// Every production receipt vault exposes `owner()`; the token-invariant
/// checks only need the getter, not the transfer/renounce mutators. This
/// narrow surface avoids depending on a richer token-side interface that
/// could drift.
interface IOwnable {
/// @notice The current owner of the contract.
/// @return The owner address.
function owner() external view returns (address);
}
52 changes: 52 additions & 0 deletions src/lib/LibInvariants.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: LicenseRef-DCL-1.0
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity ^0.8.25;

import {IGnosisSafe} from "../interface/IGnosisSafe.sol";
import {LibSafeInvariants} from "./LibSafeInvariants.sol";
import {LibTokenInvariants} from "./LibTokenInvariants.sol";

/// @title LibInvariants
/// @notice Orchestrator that composes every per-facet `assertAll` into a
/// single bundle. Each facet lib (`LibSafeInvariants`, `LibTokenInvariants`,
/// any future `Lib<Subject>Invariants`) owns its own `assertAll`; this lib
/// chains them so a consumer asserting the full production state has a
/// single call site without any facet lib having to know about other
/// facets.
/// @dev Lives separately from `LibSafeInvariants` so the file name doesn't
/// lie about scope: cross-facet composition belongs in a cross-facet lib,
/// not inside a Safe-named lib. Per-facet libs stay focused on their
/// subject and reachable standalone for scripts / fork tests that don't
/// need the full bundle.
library LibInvariants {
/// @notice Full production-state invariant bundle. Composes every
/// per-facet `assertAll`: Safe identity / config + token-side
/// owner/authoriser uniformity. Pre-flight at the start of every
/// migration script and prod-state fork test; if this passes silently
/// the live system is in its current expected state across every
/// pinned facet.
/// @dev The full-args overload is the right call site only when a
/// caller is *deliberately* asserting a state that diverges from the
/// pinned current truth (e.g. a migration script's post-state re-check
/// after it has simulated `changeThreshold`); the no-arg overload
/// fills in the `LibSafeInvariants`-pinned defaults.
/// @param safe The Safe to validate against the pinned current truth.
function assertAll(IGnosisSafe safe) internal view {
LibSafeInvariants.assertAll(safe);
LibTokenInvariants.assertAll(address(safe), LibTokenInvariants.STOX_PROD_AUTHORISER);
}

/// @notice Full-args bundle. Use when overriding the Safe-side
/// threshold or owner set from `LibSafeInvariants`' current-truth pins —
/// typically only when running a script that intentionally changes
/// one of those (post-state assertion). The token-side leg uses the
/// pinned defaults (vault ownership against the Safe, authoriser
/// against `LibTokenInvariants.STOX_PROD_AUTHORISER`).
/// @param safe The Safe to validate.
/// @param expectedThreshold The expected signature threshold.
/// @param expectedOwners The expected owner set in `getOwners()` order.
function assertAll(IGnosisSafe safe, uint256 expectedThreshold, address[] memory expectedOwners) internal view {
LibSafeInvariants.assertAll(safe, expectedThreshold, expectedOwners);
LibTokenInvariants.assertAll(address(safe), LibTokenInvariants.STOX_PROD_AUTHORISER);
}
}
139 changes: 0 additions & 139 deletions src/lib/LibProdSafes.sol

This file was deleted.

Loading
Loading