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
38 changes: 14 additions & 24 deletions script/20260619-deploy-v4-authoriser-clone.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,13 @@ error GrantsSliceOutOfRange(uint256 startIndex, uint256 sliceLength, uint256 gra
/// dispatch mechanism.
contract DeployV4AuthoriserClone is Script {
/// @notice The starting index of the non-admin grant slice inside
/// `LibAuthoriserInvariants.expectedGrants()`. Indices 0..4 are the
/// V3-era `_ADMIN` grants which the base `initialize` auto-grants on
/// the freshly-cloned V4 authoriser. Indices 5..10 are the
/// operational grants (`DEPOSIT` / `WITHDRAW` / `CERTIFY` × service +
/// Safe) this script mirrors in.
uint256 internal constant MIRROR_START_INDEX = 5;
/// `LibAuthoriserInvariants.expectedGrants()`. Indices 0..6 are the
/// seven `_ADMIN` grants (five auto-granted by the base `initialize`
/// on the freshly-cloned V4 authoriser, plus the two corporate-action
/// admins the ST0x override adds — all transferred to the Safe by
/// steps 3-4). Indices 7..12 are the operational grants (`DEPOSIT` /
/// `WITHDRAW` / `CERTIFY` × service + Safe) this script mirrors in.
uint256 internal constant MIRROR_START_INDEX = 7;

/// @notice The number of non-admin grants this script mirrors in.
uint256 internal constant MIRROR_COUNT = 6;
Expand Down Expand Up @@ -287,30 +288,20 @@ contract DeployV4AuthoriserClone is Script {
address safe = LibSafeInvariants.safeForChainId(block.chainid);
RoleGrant[] memory allGrants = LibAuthoriserInvariants.expectedGrants(safe);

// Every `(role, grantee)` in the chain's grant map holds. Covers the
// five V3-era admin grants (swapped onto the Safe in step 3) AND
// the six operational grants from step 2 in one sweep.
// Every `(role, grantee)` in the chain's 13-entry grant map holds:
// all seven `_ADMIN` roles on the Safe (swapped there in step 3) AND
// the six operational grants from step 2, in one sweep.
for (uint256 i = 0; i < allGrants.length; i++) {
if (!acl.hasRole(allGrants[i].role, allGrants[i].grantee)) {
revert ExpectedGrantMissing(allGrants[i].role, allGrants[i].grantee);
}
}

bytes32[AUTO_GRANTED_ADMIN_COUNT] memory adminRoles = autoGrantedAdminRoles();

// The Safe holds every auto-granted `_ADMIN` role — including the
// two corporate-action admins that `expectedGrants()` doesn't
// carry (V4-only roles the override's `initialize` adds).
for (uint256 i = 0; i < adminRoles.length; i++) {
if (!acl.hasRole(adminRoles[i], safe)) {
revert ExpectedGrantMissing(adminRoles[i], safe);
}
}

// The deployer holds none of the auto-granted `_ADMIN` roles.
// If any survived step 4, the deployer key still has root
// privileges over that role's grant map — closes the
// "transitional trust window" for those specific roles.
bytes32[AUTO_GRANTED_ADMIN_COUNT] memory adminRoles = autoGrantedAdminRoles();
for (uint256 i = 0; i < adminRoles.length; i++) {
if (acl.hasRole(adminRoles[i], deployer)) {
revert DeployerStillHoldsAdminRole(adminRoles[i], deployer);
Expand All @@ -321,10 +312,9 @@ contract DeployV4AuthoriserClone is Script {
/// @notice The seven `_ADMIN` roles the base + ST0x-override
/// `initialize` grant to the supplied `initialAdmin` config.
/// Hand-listed (in source-order of the `_grantRole` calls in the
/// impl) rather than derived from `expectedGrants()` because the
/// auto-grants overlap with — but are not identical to — the lib
/// map's indices 0..4: the V3-era map is missing the two
/// corporate-action admins the V4 override adds.
/// impl) because the grant/renounce sequence operates on the
/// TRANSIENT deployer-held roles, not the master map's Safe-held
/// entries.
/// @return roles The seven role hashes, in `_grantRole` order.
function autoGrantedAdminRoles() internal pure returns (bytes32[AUTO_GRANTED_ADMIN_COUNT] memory roles) {
roles[0] = keccak256("CERTIFY_ADMIN");
Expand Down
40 changes: 11 additions & 29 deletions script/20260623-upgrade-receipt-vaults-to-v4.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -304,35 +304,19 @@ contract UpgradeReceiptVaultsToV4 is Script {
V4_AUTHORISER_CLONE, LibProdDeployV4.STOX_PROD_AUTHORISER_V4_CLONE_CODEHASH, cloneCodehash
);
}
// The master `expectedGrants()` map holds on the clone — 13 entries
// covering all seven `_ADMIN` roles on the Safe (including the two
// V4-only corporate-action admins; without them the swapped clone
// can't admin corporate actions, so this is the enforcement point
// that must reject a mis-configured clone) plus the six operational
// action roles.
IAccessControl cloneAcl = IAccessControl(V4_AUTHORISER_CLONE);
RoleGrant[] memory expected = LibAuthoriserInvariants.expectedGrants();
for (uint256 i = 0; i < expected.length; i++) {
if (!cloneAcl.hasRole(expected[i].role, expected[i].grantee)) {
revert V4AuthoriserCloneExpectedGrantMissing(V4_AUTHORISER_CLONE, expected[i].role, expected[i].grantee);
}
}

// Verify all seven auto-granted `_ADMIN` roles hold on the Safe —
// including the two V4-only corporate-action admins
// (`SCHEDULE_/CANCEL_CORPORATE_ACTION_ADMIN`) that `expectedGrants()`
// doesn't carry. Without them the swapped clone can't admin corporate
// actions, so this is the enforcement point that must reject a clone
// deployed missing them.
bytes32[7] memory adminRoles = [
keccak256("CERTIFY_ADMIN"),
keccak256("CONFISCATE_RECEIPT_ADMIN"),
keccak256("CONFISCATE_SHARES_ADMIN"),
keccak256("DEPOSIT_ADMIN"),
keccak256("WITHDRAW_ADMIN"),
keccak256("SCHEDULE_CORPORATE_ACTION_ADMIN"),
keccak256("CANCEL_CORPORATE_ACTION_ADMIN")
];
address ownerSafe = LibSafeInvariants.STOX_TOKEN_OWNER_SAFE;
for (uint256 i = 0; i < adminRoles.length; i++) {
if (!cloneAcl.hasRole(adminRoles[i], ownerSafe)) {
revert V4AuthoriserCloneExpectedGrantMissing(V4_AUTHORISER_CLONE, adminRoles[i], ownerSafe);
}
}
}

/// @notice Build the bundle: one `upgradeTo(V4 impl)` per V1 beacon
Expand Down Expand Up @@ -361,9 +345,8 @@ contract UpgradeReceiptVaultsToV4 is Script {
/// @notice Post-state assertions after the upgrade + swap simulate: all
/// three beacons are at V4 and Safe-owned, every production receipt vault
/// reports the V4 clone as its authoriser, and the Safe identity +
/// threshold are unchanged. Split from `run()` so tests can drive it
/// against a deliberately-malformed post-state (e.g. an un-swapped vault)
/// and assert the `VaultAuthoriserMismatchPostUpgrade` guard fires.
/// threshold are unchanged. Split from `run()` to keep the post-state
/// assertions separately readable from the bundle construction.
/// @param safe The ST0x token-owner Safe.
/// @param vaults The production receipt vaults the swap targets.
function _assertPostState(IGnosisSafe safe, address[] memory vaults) internal view {
Expand All @@ -384,10 +367,9 @@ contract UpgradeReceiptVaultsToV4 is Script {
}
}
// Safe identity + threshold unchanged. (The explicit per-vault
// V4-clone loop above is deliberately STRICTER than the
// migration-window authoriser leg in `LibInvariants.assertAll` —
// this is the post-state of the swap itself, so only the V4 clone
// is acceptable regardless of the window. The Safe-side legs are
// V4-clone loop above mirrors the authoriser leg in
// `LibInvariants.assertAll`, which also accepts only the V4 clone.
// The Safe-side legs are
// asserted piecemeal to avoid re-running the token-side legs the
// loop above already covers.)
LibSafeInvariants.assertImmutableInvariants(safe);
Expand Down
Loading
Loading