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
28 changes: 14 additions & 14 deletions src/lib/LibInvariants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,25 +57,26 @@ library LibInvariants {

/// @notice Multichain full-production-state pre-flight — the
/// chain-agnostic generalisation of `assertAll(safe)`. Asserts, for the
/// ACTIVE chain (`block.chainid`): the Safe carries Base's shared policy
/// (`assertPolicyMatchesBase` — v1.4.1 identity, owner SET, threshold), the
/// token-side uniformity (every vault in `tokens` owned by that chain's
/// Safe and gated by the single `authoriser`), and the authoriser's role-
/// grant map for that chain's Safe. The Safe is resolved from the pinned
/// per-chain address via `LibSafeInvariants.safeForChainId(block.chainid)`,
/// ACTIVE chain (`block.chainid`): the Safe carries the chain-agnostic
/// token-owner policy (`assertTokenOwnerSafePolicy` — v1.4.1 identity,
/// owner SET, threshold), the token-side uniformity (every vault in
/// `tokens` owned by that chain's Safe and gated by the single
/// `authoriser`), and the authoriser's role-grant map for that chain's
/// Safe. The Safe is resolved AND policy-asserted in one call via
/// `LibSafeInvariants.assertActiveChainTokenOwnerSafe(block.chainid)`,
/// so the deploy artifacts that differ per chain — the Safe address, the
/// token addresses, the authoriser clone address — are the only variation.
///
/// @dev The Safe POLICY (owner set, threshold, v1.4.1 identity) and the
/// service signer are SHARED across chains; only the ADDRESSES differ. The
/// Safe address is therefore a per-chain deploy artifact (not a principal):
/// resolved by chain id, and its policy asserted against the shared pins.
/// The owner check is order-INSENSITIVE (`assertPolicyMatchesBase`) because
/// a fresh per-chain Safe's `getOwners()` order is incidental. There is no
/// `ChainPrincipals` parameter — the per-chain inputs are the token
/// addresses and the authoriser clone address (whose impl codehash is
/// asserted equal across chains by the cross-chain parity pin); the Safe
/// address is read from the per-chain pin here.
/// The owner check is order-INSENSITIVE because a per-chain Safe's
/// `getOwners()` order is incidental. There is no `ChainPrincipals`
/// parameter — the per-chain inputs are the token addresses and the
/// authoriser clone address (whose impl codehash is asserted equal across
/// chains by the cross-chain parity pin); the Safe address is read from
/// the per-chain pin here.
///
/// Unlike Base's `assertAll(safe)` this asserts a SINGLE uniform
/// authoriser rather than the V4 swap-window pair: a bootstrap chain is
Expand All @@ -87,8 +88,7 @@ library LibInvariants {
/// @param tokens The chain's production token table.
/// @param authoriser The chain's live authoriser the vaults point at.
function assertProductionState(TokenInstance[] memory tokens, address authoriser) internal view {
address safe = LibSafeInvariants.safeForChainId(block.chainid);
LibSafeInvariants.assertPolicyMatchesBase(IGnosisSafe(safe));
address safe = LibSafeInvariants.assertActiveChainTokenOwnerSafe(block.chainid);
LibTokenInvariants.assertAll(tokens, safe, authoriser);
LibAuthoriserInvariants.assertExpectedGrants(authoriser, safe);
}
Expand Down
57 changes: 39 additions & 18 deletions src/lib/LibSafeInvariants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ library LibSafeInvariants {
/// artifact, NOT a principal; the whole POLICY (owners, threshold, v1.4.1
/// identity, fallback handler, no modules/guard) is the shared pin set, and
/// this Safe is asserted against it — in every way that matters, now and
/// into the future — by `assertPolicyMatchesBase` (order-insensitive on the
/// into the future — by `assertTokenOwnerSafePolicy` (order-insensitive on the
/// owner set; L1/L2-tolerant on the singleton, since a mainnet Safe runs
/// the L1 `Safe` singleton while Base runs the L2 `SafeL2`).
address internal constant STOX_TOKEN_OWNER_SAFE_ETHEREUM = 0x3840aeDaEc8e82f79d8F6a8F6ADCa271E13E0329;
Expand Down Expand Up @@ -509,25 +509,46 @@ library LibSafeInvariants {
}
}

/// @notice Assert a chain's Safe carries the SAME policy as Base — in every
/// way that matters: the v1.4.1 immutable identity (proxy codehash,
/// singleton pointer + bytecode, version, no modules, no guard, pinned
/// fallback handler), the same owner SET, and the same threshold — as
/// pinned in this library (which is Base's current truth). The owner check
/// is order-INSENSITIVE (`assertOwnerSetUnordered`), because the only thing
/// that legitimately differs across chains is the Safe ADDRESS (and, as a
/// consequence of a fresh deploy, the incidental `getOwners()` order).
/// @dev This is the cross-chain / non-baseline-chain bundle. Base's own
/// pin test uses the order-SENSITIVE `assertAll` against its pinned roster.
/// Because the pins are the single source of truth for the policy and Base
/// is asserted against them too, asserting a chain's Safe here transitively
/// proves it matches Base — now, and on every scheduled CI run into the
/// future (if Base's policy changes, the pins move and this goes red until
/// the chain's Safe is realigned).
/// @param safe The chain's Safe to validate against Base's shared policy.
function assertPolicyMatchesBase(IGnosisSafe safe) internal view {
/// @notice Assert a Safe carries the ST0x token-owner POLICY — the
/// chain-agnostic truths pinned in this library: the v1.4.1 immutable
/// identity (proxy codehash, singleton pointer + bytecode, version, no
/// modules, no guard, pinned fallback handler), the pinned owner SET, and
/// the pinned threshold. The policy is a property of the ORGANISATION,
/// not of any chain: every chain's token-owner Safe — Base included — is
/// asserted against the same pins. Only the Safe ADDRESS is per-chain (a
/// deploy artifact, resolved by `safeForChainId`).
/// @dev The owner check is order-INSENSITIVE (`assertOwnerSetUnordered`):
/// `getOwners()` order is a Safe-internal linked-list artifact of the
/// order owners were added on that chain's deploy/rotation, so order is
/// not a policy property. (Base's own historical pin test additionally
/// asserts its exact roster order via the order-sensitive `assertAll`.)
/// If the policy ever changes, the pins move and every chain's Safe goes
/// red until realigned — one source of truth, asserted everywhere, on
/// every scheduled CI run.
/// @param safe The Safe to validate against the pinned policy.
function assertTokenOwnerSafePolicy(IGnosisSafe safe) internal view {
assertImmutableInvariants(safe);
assertOwnerSetUnordered(safe, expectedOwners());
assertThreshold(safe, STOX_TOKEN_OWNER_SAFE_THRESHOLD);
}

/// @notice Resolve the active chain's token-owner Safe AND assert it
/// carries the pinned chain-agnostic policy
/// (`assertTokenOwnerSafePolicy`). No chain is special-cased: the policy
/// is the shared truth and every chain's Safe is asserted against it
/// identically. This is the single entry point a broadcast script's
/// pre-flight and the scheduled CI pin both call, so the assertion that
/// gates a manual broadcast is the identical one CI runs every commit — a
/// broadcast can never revert on a Safe check CI has not already
/// exercised on that chain.
///
/// Reverts `UnsupportedChainForTokenOwnerSafe` (via `safeForChainId`) for a
/// chain without a pinned Safe rather than silently asserting the wrong
/// chain's Safe.
/// @param chainId The active chain id (`block.chainid`).
/// @return safe The chain's token-owner Safe, proven in-policy.
function assertActiveChainTokenOwnerSafe(uint256 chainId) internal view returns (address safe) {
safe = safeForChainId(chainId);
assertTokenOwnerSafePolicy(IGnosisSafe(safe));
}
}
8 changes: 5 additions & 3 deletions test/src/concrete/deploy/EthereumTokenOwnerSafeParity.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ contract EthereumTokenOwnerSafeParityTest is Test {

vm.createSelectFork(LibStoxDeployNetworks.ETHEREUM);

// Matches Base's policy in every way that matters: v1.4.1 identity,
// owner set (order-insensitive), threshold.
LibSafeInvariants.assertPolicyMatchesBase(IGnosisSafe(ethSafe));
// Carries the chain-agnostic token-owner policy in every way that
// matters: v1.4.1 identity, owner set (order-insensitive), threshold.
// Base is asserted against the same pins, so this transitively proves
// parity with Base.
LibSafeInvariants.assertTokenOwnerSafePolicy(IGnosisSafe(ethSafe));
}
}
26 changes: 26 additions & 0 deletions test/src/lib/LibSafeInvariants.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {LibSafeInvariants} from "../../../src/lib/LibSafeInvariants.sol";
import {LibSafeInvariantsHarness} from "./LibSafeInvariantsHarness.sol";
import {IGnosisSafe} from "../../../src/interface/IGnosisSafe.sol";
import {LibRainDeploy} from "rain-deploy-0.1.4/src/lib/LibRainDeploy.sol";
import {LibStoxDeployNetworks} from "../../../src/lib/LibStoxDeployNetworks.sol";
import {
SafeProxyCodehashMismatch,
SafeSingletonMismatch,
Expand Down Expand Up @@ -275,4 +276,29 @@ contract LibSafeInvariantsTest is Test {
vm.expectRevert(abi.encodeWithSelector(SafeThresholdMismatch.selector, address(safe), uint256(4), uint256(3)));
harness.callAssertAll(safe, 4, LibSafeInvariants.expectedOwners());
}

/// @notice `assertActiveChainTokenOwnerSafe` passes against the LIVE Base
/// token-owner Safe and resolves Base's Safe address — the same
/// chain-agnostic policy assertion every chain gets.
function testAssertActiveChainTokenOwnerSafeOnBase() external {
vm.createSelectFork(LibRainDeploy.BASE);
address resolved = LibSafeInvariants.assertActiveChainTokenOwnerSafe(block.chainid);
assertEq(resolved, LibSafeInvariants.STOX_TOKEN_OWNER_SAFE, "Base resolved the wrong Safe");
}

/// @notice The SAME entry point passes against the LIVE Ethereum token-
/// owner Safe and resolves Ethereum's Safe address. The policy pins
/// (owner SET, threshold, v1.4.1 identity) are chain-agnostic truths;
/// only the Safe address is per-chain, and `getOwners()` order is an
/// incidental linked-list artifact of each chain's own deploy — which is
/// why the policy's owner check is order-insensitive. This per-chain fork
/// coverage is what keeps a broadcast script's Safe pre-flight from ever
/// reverting on a chain CI has not exercised: any consumer of this entry
/// point is proven against every pinned chain's live Safe on every CI
/// run.
function testAssertActiveChainTokenOwnerSafeOnEthereum() external {
vm.createSelectFork(LibStoxDeployNetworks.ETHEREUM);
address resolved = LibSafeInvariants.assertActiveChainTokenOwnerSafe(block.chainid);
assertEq(resolved, LibSafeInvariants.STOX_TOKEN_OWNER_SAFE_ETHEREUM, "Ethereum resolved the wrong Safe");
}
}
Loading