Skip to content
Open
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
4 changes: 2 additions & 2 deletions script/20260716-migrate-beacon-owners-ethereum.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ contract MigrateBeaconOwnersEthereum is Script {
function run() external {
address safe = LibSafeInvariants.STOX_TOKEN_OWNER_SAFE_ETHEREUM;
require(safe != address(0), "Ethereum token-owner Safe not pinned");
address[3] memory beaconList = LibProdBeacons0_1_1.beacons();
address[3] memory implList = LibProdBeacons0_1_1.implementations();
address[4] memory beaconList = LibProdBeacons0_1_1.beacons();
address[4] memory implList = LibProdBeacons0_1_1.implementations();

// Pre-flight: every beacon is deployed, is the OZ UpgradeableBeacon,
// is still owned by the deploy EOA, and points at its pinned impl.
Expand Down
4 changes: 2 additions & 2 deletions script/20260722-migrate-beacon-owners-hyperevm.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ contract MigrateBeaconOwnersHyperEvm is Script {
address safe = LibSafeInvariants.STOX_TOKEN_OWNER_SAFE_HYPEREVM;
require(safe != address(0), "HyperEVM token-owner Safe not pinned");
require(block.chainid == LibSafeInvariants.HYPEREVM_CHAIN_ID, "not HyperEVM - wrong --rpc-url");
address[3] memory beaconList = LibProdBeacons0_1_1.beacons();
address[3] memory implList = LibProdBeacons0_1_1.implementations();
address[4] memory beaconList = LibProdBeacons0_1_1.beacons();
address[4] memory implList = LibProdBeacons0_1_1.implementations();

// Pre-flight: every beacon is deployed, is the OZ UpgradeableBeacon,
// is still owned by the deploy EOA, and points at its pinned impl.
Expand Down
37 changes: 26 additions & 11 deletions script/20260729-migrate-governance-to-timelock.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {IBeacon} from "@openzeppelin-contracts-5.6.1/proxy/beacon/IBeacon.sol";
import {IGnosisSafe} from "../src/interface/IGnosisSafe.sol";
import {LibAuthoriserInvariants, RoleGrant} from "../src/lib/LibAuthoriserInvariants.sol";
import {LibBeaconInvariants} from "../src/lib/LibBeaconInvariants.sol";
import {LibOrchestratorInvariants} from "../src/lib/LibOrchestratorInvariants.sol";
import {LibProdDeployV4} from "../src/generated/LibProdDeployV4.sol";
import {LibSafeInvariants} from "../src/lib/LibSafeInvariants.sol";
import {LibSafeOps, SafeTx} from "../src/lib/LibSafeOps.sol";
Expand Down Expand Up @@ -308,7 +309,7 @@ contract MigrateGovernanceToTimelock is Script {

// What the beacons serve, captured before the bundle so the
// post-state can prove the migration moved ownership only.
address[3] memory beaconImplsBefore = _beaconImplementations();
address[4] memory beaconImplsBefore = _beaconImplementations();

// --- Build the bundle ---------------------------------------------

Expand Down Expand Up @@ -447,14 +448,14 @@ contract MigrateGovernanceToTimelock is Script {
address timelock,
address authoriser,
TokenInstance[] memory tokens,
address[3] memory beaconImplsBefore
address[4] memory beaconImplsBefore
) internal view {
LibTokenInvariants.assertUniformOwnership(tokens, timelock);
LibTokenInvariants.assertUniformAuthoriser(tokens, authoriser);

LibBeaconInvariants.assertProdBeaconsOwnedBy(block.chainid, timelock);
address[3] memory beaconsAfter = LibBeaconInvariants.prodBeaconsForChainId(block.chainid);
address[3] memory beaconImplsAfter = _beaconImplementations();
address[4] memory beaconsAfter = LibBeaconInvariants.prodBeaconsForChainId(block.chainid);
address[4] memory beaconImplsAfter = _beaconImplementations();
for (uint256 i = 0; i < beaconsAfter.length; i++) {
if (beaconImplsAfter[i] != beaconImplsBefore[i]) {
revert BeaconImplementationMoved(beaconsAfter[i], beaconImplsBefore[i], beaconImplsAfter[i]);
Expand Down Expand Up @@ -550,20 +551,34 @@ contract MigrateGovernanceToTimelock is Script {
/// @return targets The still-Safe-owned in-use beacons, in fixed
/// (receipt, receipt vault, wrapped token vault) order.
function _selectBeaconTargets(address safe, address timelock) internal view returns (address[] memory targets) {
address[3] memory beacons = LibBeaconInvariants.prodBeaconsForChainId(block.chainid);
address[4] memory beacons = LibBeaconInvariants.prodBeaconsForChainId(block.chainid);
// Index-aligned: the set spans two build generations, so one pin
// cannot cover it. See `prodBeaconCodehashesForChainId`.
bytes32[4] memory expectedCodehashes = LibBeaconInvariants.prodBeaconCodehashesForChainId(block.chainid);
address[] memory candidates = new address[](beacons.length);
uint256 count = 0;
for (uint256 i = 0; i < beacons.length; i++) {
bytes32 codehash = beacons[i].codehash;
if (codehash != LibBeaconInvariants.UPGRADEABLE_BEACON_CODEHASH) {
revert MigrationBeaconCodehashMismatch(
beacons[i], LibBeaconInvariants.UPGRADEABLE_BEACON_CODEHASH, codehash
);
if (codehash != expectedCodehashes[i]) {
revert MigrationBeaconCodehashMismatch(beacons[i], expectedCodehashes[i], codehash);
}
address actual = Ownable(beacons[i]).owner();
if (actual == timelock) {
continue;
}
// The orchestrator beacon joins the governed set mid-lifecycle:
// until its own EOA -> Safe migration
// (20260818-migrate-orchestrator-beacon-owner) executes, it is
// not the Safe's to move — skip it; a later dispatch of this
// script picks it up once Safe-owned. After that migration's
// deadline the baked EOA stops being an accepted state and the
// refusal below fires.
if (
i == LibBeaconInvariants.ORCHESTRATOR_BEACON_INDEX && actual == LibProdDeployV4.BEACON_INITIAL_OWNER
&& block.timestamp < LibOrchestratorInvariants.ST0X_ORCHESTRATOR_BEACON_OWNER_MIGRATION_DEADLINE
) {
continue;
}
if (actual != safe) {
revert UnexpectedBeaconOwner(beacons[i], actual);
}
Expand All @@ -583,8 +598,8 @@ contract MigrateGovernanceToTimelock is Script {
/// would propagate to every production proxy on the chain.
/// @return impls The three current implementations, index-aligned with
/// `prodBeaconsForChainId`.
function _beaconImplementations() internal view returns (address[3] memory impls) {
address[3] memory beacons = LibBeaconInvariants.prodBeaconsForChainId(block.chainid);
function _beaconImplementations() internal view returns (address[4] memory impls) {
address[4] memory beacons = LibBeaconInvariants.prodBeaconsForChainId(block.chainid);
for (uint256 i = 0; i < beacons.length; i++) {
impls[i] = IBeacon(beacons[i]).implementation();
}
Expand Down
4 changes: 2 additions & 2 deletions script/20260825-upgrade-fleet-to-0-1-30.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ contract UpgradeFleetTo0_1_30 is Script {
/// @param beacons The chain's in-use beacons (receipt, receipt vault,
/// wrapped token vault) — index order pinned by `LibProdBeacons*`.
/// @return txs The self-scoped upgrade transactions.
function authorBundle(address[3] memory beacons) internal view returns (SafeTx[] memory txs) {
function authorBundle(address[4] memory beacons) internal view returns (SafeTx[] memory txs) {
address[2] memory gated = [
beacons[LibBeaconInvariants.RECEIPT_BEACON_INDEX], beacons[LibBeaconInvariants.RECEIPT_VAULT_BEACON_INDEX]
];
Expand Down Expand Up @@ -213,7 +213,7 @@ contract UpgradeFleetTo0_1_30 is Script {

// The in-use beacons are deployed, OZ bytecode, Safe-owned.
LibBeaconInvariants.assertProdBeaconsOwnedByChainSafe(block.chainid);
address[3] memory beacons = LibBeaconInvariants.prodBeaconsForChainId(block.chainid);
address[4] memory beacons = LibBeaconInvariants.prodBeaconsForChainId(block.chainid);

// --- Build the bundle ----------------------------------------------

Expand Down
2 changes: 1 addition & 1 deletion script/20260831-enable-orchestrator-roles.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ contract EnableOrchestratorRoles is Script {
// Index order pinned by LibProdBeacons*: receipt, receipt vault,
// wrapped token vault. The wrapped-token-vault beacon is not part of
// the orchestrator's surface and is not gated here.
address[3] memory beacons = LibBeaconInvariants.prodBeaconsForChainId(block.chainid);
address[4] memory beacons = LibBeaconInvariants.prodBeaconsForChainId(block.chainid);
address receiptImpl = IBeacon(beacons[0]).implementation();
if (receiptImpl != LibProdDeployV4.STOX_RECEIPT_0_1_30) {
revert FleetNotUpgraded(beacons[0], LibProdDeployV4.STOX_RECEIPT_0_1_30, receiptImpl);
Expand Down
66 changes: 57 additions & 9 deletions src/lib/LibBeaconInvariants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ pragma solidity ^0.8.25;

import {IBeacon} from "@openzeppelin-contracts-5.6.1/proxy/beacon/IBeacon.sol";
import {LibMigrationInvariant} from "./LibMigrationInvariant.sol";
import {LibOrchestratorInvariants} from "./LibOrchestratorInvariants.sol";
import {LibProdDeployV4} from "../generated/LibProdDeployV4.sol";
import {LibProdBeaconsBase} from "./LibProdBeaconsBase.sol";
import {LibProdBeacons0_1_1} from "./LibProdBeacons0_1_1.sol";
import {LibSafeInvariants} from "./LibSafeInvariants.sol";
Expand Down Expand Up @@ -230,6 +232,26 @@ library LibBeaconInvariants {
/// `prodBeaconsForChainId`.
uint256 internal constant WRAPPED_TOKEN_VAULT_BEACON_INDEX = 2;

/// @notice Position of the orchestrator beacon in `prodBeaconsForChainId`.
uint256 internal constant ORCHESTRATOR_BEACON_INDEX = 3;

/// @notice Expected runtime codehash of each beacon in
/// `prodBeaconsForChainId`, index-aligned with it.
/// @dev The set spans two build generations and a single pin cannot cover
/// both: the three token beacons are the V1 build (858 bytes), the
/// orchestrator beacon is OZ 5.6.1 at the current `optimizer_runs`
/// (728 bytes). Asserting the whole set against one constant reverts on
/// the orchestrator beacon and takes every chain's migration with it.
/// @return The expected codehash per beacon.
function prodBeaconCodehashesForChainId(uint256) internal pure returns (bytes32[4] memory) {
return [
UPGRADEABLE_BEACON_CODEHASH,
UPGRADEABLE_BEACON_CODEHASH,
UPGRADEABLE_BEACON_CODEHASH,
UPGRADEABLE_BEACON_CODEHASH_0_1_30
];
}

/// @notice The three production beacons IN USE on the active chain, in a
/// fixed order (receipt, receipt vault, wrapped token vault). Beacon
/// addresses are per-chain deploy artifacts that never change once a
Expand All @@ -240,7 +262,7 @@ library LibBeaconInvariants {
/// order); this map only dispatches by chain id.
/// @param chainId The active chain id (`block.chainid`).
/// @return The chain's three in-use beacon addresses.
function prodBeaconsForChainId(uint256 chainId) internal view returns (address[3] memory) {
function prodBeaconsForChainId(uint256 chainId) internal view returns (address[4] memory) {
if (chainId == LibSafeInvariants.BASE_CHAIN_ID) {
return LibProdBeaconsBase.beacons();
}
Expand Down Expand Up @@ -285,10 +307,24 @@ library LibBeaconInvariants {
/// @param expectedOwner The address every in-use beacon must report as
/// `owner()`.
function assertProdBeaconsOwnedBy(uint256 chainId, address expectedOwner) internal view {
address[3] memory beacons = prodBeaconsForChainId(chainId);
address[4] memory beacons = prodBeaconsForChainId(chainId);
bytes32[4] memory codehashes = prodBeaconCodehashesForChainId(chainId);
for (uint256 i = 0; i < beacons.length; i++) {
_assertDeployedPinnedBeacon(beacons[i]);
_assertDeployedPinnedBeacon(beacons[i], codehashes[i]);
address actualOwner = IOwnable(beacons[i]).owner();
// The orchestrator beacon joins the governed set mid-lifecycle:
// its constructor bakes the deploy EOA as owner and the
// `20260818-migrate-orchestrator-beacon-owner` EOA broadcast
// moves it to the chain's Safe. Until that migration's deadline,
// the EOA is an accepted pre-state; after it, only the expected
// owner passes — the same window `LibOrchestratorInvariants.
// assertBeaconSet` applies.
if (
i == ORCHESTRATOR_BEACON_INDEX && actualOwner == LibProdDeployV4.BEACON_INITIAL_OWNER
&& block.timestamp < LibOrchestratorInvariants.ST0X_ORCHESTRATOR_BEACON_OWNER_MIGRATION_DEADLINE
) {
continue;
}
if (actualOwner != expectedOwner) {
revert BeaconOwnerMismatch(beacons[i], expectedOwner, actualOwner);
}
Expand Down Expand Up @@ -327,10 +363,22 @@ library LibBeaconInvariants {
internal
view
{
address[3] memory beacons = prodBeaconsForChainId(chainId);
address[4] memory beacons = prodBeaconsForChainId(chainId);
bytes32[4] memory codehashes = prodBeaconCodehashesForChainId(chainId);
for (uint256 i = 0; i < beacons.length; i++) {
_assertDeployedPinnedBeacon(beacons[i]);
LibMigrationInvariant.assertMigration("beacon.owner()", IOwnable(beacons[i]).owner(), pre, post, deadline);
_assertDeployedPinnedBeacon(beacons[i], codehashes[i]);
address actualOwner = IOwnable(beacons[i]).owner();
// See `assertProdBeaconsOwnedBy`: the orchestrator beacon's own
// EOA -> Safe migration window overlaps the governance window,
// so its baked initial owner is an accepted extra pre-state
// until that migration's deadline.
if (
i == ORCHESTRATOR_BEACON_INDEX && actualOwner == LibProdDeployV4.BEACON_INITIAL_OWNER
&& block.timestamp < LibOrchestratorInvariants.ST0X_ORCHESTRATOR_BEACON_OWNER_MIGRATION_DEADLINE
) {
continue;
}
LibMigrationInvariant.assertMigration("beacon.owner()", actualOwner, pre, post, deadline);
}
}

Expand All @@ -341,13 +389,13 @@ library LibBeaconInvariants {
/// selector and report whatever owner passes the caller's check, so
/// the pin is what makes the subsequent read meaningful.
/// @param beacon The beacon to gate.
function _assertDeployedPinnedBeacon(address beacon) private view {
function _assertDeployedPinnedBeacon(address beacon, bytes32 expectedCodehash) private view {
if (beacon.code.length == 0) {
revert BeaconNotDeployed(beacon);
}
bytes32 actualCodehash = beacon.codehash;
if (actualCodehash != UPGRADEABLE_BEACON_CODEHASH) {
revert BeaconCodehashMismatch(beacon, UPGRADEABLE_BEACON_CODEHASH, actualCodehash);
if (actualCodehash != expectedCodehash) {
revert BeaconCodehashMismatch(beacon, expectedCodehash, actualCodehash);
}
}
}
10 changes: 6 additions & 4 deletions src/lib/LibProdBeacons0_1_1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,14 @@ library LibProdBeacons0_1_1 {
/// generated `0_1_1` pin. `view` because the first two are live reads from
/// the deployer (which is why callers run against an Ethereum fork).
/// @return The three Ethereum beacon addresses.
function beacons() internal view returns (address[3] memory) {
function beacons() internal view returns (address[4] memory) {
IST0xVaultBeaconSet deployer =
IST0xVaultBeaconSet(LibProdDeployV4.STOX_OFFCHAIN_ASSET_RECEIPT_VAULT_BEACON_SET_DEPLOYER_0_1_1);
return [
address(deployer.iReceiptBeacon()),
address(deployer.iOffchainAssetReceiptVaultBeacon()),
LibProdDeployV4.STOX_WRAPPED_TOKEN_VAULT_BEACON_0_1_1
LibProdDeployV4.STOX_WRAPPED_TOKEN_VAULT_BEACON_0_1_1,
LibProdDeployV4.ST0X_ORCHESTRATOR_BEACON
];
}

Expand All @@ -65,11 +66,12 @@ library LibProdBeacons0_1_1 {
/// deterministic addresses on every chain, so no separate Ethereum copy.
/// Asserted unchanged across the ownership transfer.
/// @return The three implementation addresses.
function implementations() internal pure returns (address[3] memory) {
function implementations() internal pure returns (address[4] memory) {
return [
LibProdDeployV4.STOX_RECEIPT_0_1_1,
LibProdDeployV4.STOX_RECEIPT_VAULT_0_1_1,
LibProdDeployV4.STOX_WRAPPED_TOKEN_VAULT_0_1_1
LibProdDeployV4.STOX_WRAPPED_TOKEN_VAULT_0_1_1,
LibProdDeployV4.ST0X_ORCHESTRATOR_0_1_30
];
}
}
10 changes: 6 additions & 4 deletions src/lib/LibProdBeaconsBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ library LibProdBeaconsBase {
/// receipt vault, wrapped token vault) — index-aligned with
/// `implementations()` and with `LibProdBeacons0_1_1.beacons()`.
/// @return The three Base beacon addresses.
function beacons() internal pure returns (address[3] memory) {
function beacons() internal pure returns (address[4] memory) {
return [
LibProdDeployV1.STOX_RECEIPT_BEACON_V1,
LibProdDeployV1.STOX_RECEIPT_VAULT_BEACON_V1,
LibProdDeployV1.STOX_WRAPPED_TOKEN_VAULT_BEACON_V1
LibProdDeployV1.STOX_WRAPPED_TOKEN_VAULT_BEACON_V1,
LibProdDeployV4.ST0X_ORCHESTRATOR_BEACON
];
}

Expand All @@ -42,11 +43,12 @@ library LibProdBeaconsBase {
/// same deterministic addresses `LibProdBeacons0_1_1.implementations()`
/// resolves, because implementation parity across chains is the goal.
/// @return The three implementation addresses.
function implementations() internal pure returns (address[3] memory) {
function implementations() internal pure returns (address[4] memory) {
return [
LibProdDeployV4.STOX_RECEIPT_0_1_1,
LibProdDeployV4.STOX_RECEIPT_VAULT_0_1_1,
LibProdDeployV4.STOX_WRAPPED_TOKEN_VAULT_0_1_1
LibProdDeployV4.STOX_WRAPPED_TOKEN_VAULT_0_1_1,
LibProdDeployV4.ST0X_ORCHESTRATOR_0_1_30
];
}
}
Loading
Loading