Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8675266
feat(lib): LibTimelockInvariants + authoriser grant-map admin-holder …
Jul 29, 2026
7b26cf6
feat(lib): HyperEVM governance-timelock pin and chain arm
Aug 10, 2026
218be4b
fix(test): pin the grant map at 16 entries after the restack merge
Aug 10, 2026
2ef05bf
pin(timelock): hydrate the deployed Base governance timelock
Aug 10, 2026
3366565
revert(pin): unpin the Base timelock - optimizer change moved the add…
Aug 10, 2026
e4d9411
Merge remote-tracking branch 'origin/main' into feat/20260729-timeloc…
thedavidmeister Aug 10, 2026
f415edb
fix(lib): reject retained Safe _ADMIN copies under a distinct admin h…
thedavidmeister Aug 11, 2026
1b67597
style: forge fmt collapse on the renounce mockCall
thedavidmeister Aug 11, 2026
0f09c0a
fix(lib): slice retained-admin check positionally, not by grantee add…
thedavidmeister Aug 11, 2026
b0f438f
feat(lib): LibTimelockInvariants + authoriser grant-map admin-holder …
Jul 29, 2026
71f10ac
feat(lib): HyperEVM governance-timelock pin and chain arm
Aug 10, 2026
0090c9a
fix(test): pin the grant map at 16 entries after the restack merge
Aug 10, 2026
3727645
pin(timelock): hydrate the deployed Base governance timelock
Aug 10, 2026
096339e
revert(pin): unpin the Base timelock - optimizer change moved the add…
Aug 10, 2026
78aeb1d
pin(timelock): hydrate the deployed governance timelocks on all three…
Aug 11, 2026
260f852
feat(lib): freeze the timelock bytecode as pinned constants
thedavidmeister Aug 11, 2026
98224d4
Merge remote-tracking branch 'origin/feat/20260729-timelock-invariant…
thedavidmeister Aug 11, 2026
d327750
test(lib): walk the open-role rejection across every lifecycle role
thedavidmeister Aug 11, 2026
6da08a2
test(lib): walk the missing-role rejection across every required role
thedavidmeister Aug 11, 2026
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
60 changes: 51 additions & 9 deletions src/lib/LibAuthoriserInvariants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -113,20 +113,44 @@ library LibAuthoriserInvariants {
/// grantee slots.
/// @return grants The `(role, grantee)` pairs for that chain.
function expectedGrants(address tokenOwnerSafe) internal pure returns (RoleGrant[] memory grants) {
grants = expectedGrants(tokenOwnerSafe, tokenOwnerSafe);
}

/// @notice The `(role, grantee)` map parameterised on BOTH the chain's
/// token-owner Safe AND the holder of the seven `_ADMIN` roles. Before
/// the governance-timelock migration the Safe is the admin holder (the
/// two-arg call sites above collapse the parameters); after the
/// migration the seven `_ADMIN` roles sit on the governance timelock
/// while the Safe keeps its three direct action roles. This overload is
/// the single map both states are expressed through, so the migration
/// script's post-state and the post-migration invariant surface assert
/// the same structure the pre-migration consumers do.
/// @param tokenOwnerSafe The chain's token-owner Safe filling the
/// operational Safe grantee slots.
/// @param adminHolder The holder of the seven `_ADMIN` roles (the Safe
/// pre-migration, the governance timelock post-migration).
/// @return grants The `(role, grantee)` pairs for that chain.
function expectedGrants(address tokenOwnerSafe, address adminHolder)
internal
pure
returns (RoleGrant[] memory grants)
{
grants = new RoleGrant[](16);

// Init grants (block 41715184 on Base) — Safe receives every `_ADMIN`.
grants[0] = RoleGrant(keccak256("DEPOSIT_ADMIN"), tokenOwnerSafe);
grants[1] = RoleGrant(keccak256("WITHDRAW_ADMIN"), tokenOwnerSafe);
grants[2] = RoleGrant(keccak256("CERTIFY_ADMIN"), tokenOwnerSafe);
grants[3] = RoleGrant(keccak256("CONFISCATE_SHARES_ADMIN"), tokenOwnerSafe);
grants[4] = RoleGrant(keccak256("CONFISCATE_RECEIPT_ADMIN"), tokenOwnerSafe);
// Init grants (block 41715184 on Base) — the admin holder receives
// every `_ADMIN` (the Safe at init; the governance timelock once the
// timelock migration executes).
grants[0] = RoleGrant(keccak256("DEPOSIT_ADMIN"), adminHolder);
grants[1] = RoleGrant(keccak256("WITHDRAW_ADMIN"), adminHolder);
grants[2] = RoleGrant(keccak256("CERTIFY_ADMIN"), adminHolder);
grants[3] = RoleGrant(keccak256("CONFISCATE_SHARES_ADMIN"), adminHolder);
grants[4] = RoleGrant(keccak256("CONFISCATE_RECEIPT_ADMIN"), adminHolder);

// The two corporate-action admins the 0.1.1 impl adds. On Base the
// clone-deploy broadcast transferred them to the Safe alongside the
// other five and renounced them from the deploy key.
grants[5] = RoleGrant(keccak256("SCHEDULE_CORPORATE_ACTION_ADMIN"), tokenOwnerSafe);
grants[6] = RoleGrant(keccak256("CANCEL_CORPORATE_ACTION_ADMIN"), tokenOwnerSafe);
grants[5] = RoleGrant(keccak256("SCHEDULE_CORPORATE_ACTION_ADMIN"), adminHolder);
grants[6] = RoleGrant(keccak256("CANCEL_CORPORATE_ACTION_ADMIN"), adminHolder);

// Service EOA provisioned at blocks 41797262, 41797281, 41797297 (Base).
grants[7] = RoleGrant(keccak256("DEPOSIT"), GRANTEE_SERVICE_1C66);
Expand Down Expand Up @@ -173,20 +197,38 @@ library LibAuthoriserInvariants {
/// @param tokenOwnerSafe The chain's token-owner Safe filling the Safe
/// grantee slots.
function assertExpectedGrants(address authoriser, address tokenOwnerSafe) internal view {
assertExpectedGrants(authoriser, tokenOwnerSafe, tokenOwnerSafe);
}

/// @notice Assert every `(role, grantee)` pair from
/// `expectedGrants(tokenOwnerSafe, adminHolder)` is held on the supplied
/// authoriser, and that no named principal — the Safe, the admin holder,
/// the service signer — holds `DEFAULT_ADMIN_ROLE`. This is the
/// post-timelock-migration assertion surface: the migration script's
/// post-state and the migration-window invariants call it with the
/// governance timelock as `adminHolder`.
/// @param authoriser The authoriser to validate.
/// @param tokenOwnerSafe The chain's token-owner Safe filling the
/// operational Safe grantee slots.
/// @param adminHolder The holder of the seven `_ADMIN` roles.
function assertExpectedGrants(address authoriser, address tokenOwnerSafe, address adminHolder) internal view {
IAccessControl acl = IAccessControl(authoriser);
// No pinned grantee holds DEFAULT_ADMIN_ROLE: the hierarchy admins each
// action role by its own `<ROLE>_ADMIN`, so a root-admin holder would
// be an escalation path the pinned map does not sanction.
if (acl.hasRole(DEFAULT_ADMIN_ROLE, tokenOwnerSafe)) {
revert UnexpectedDefaultAdmin(authoriser, tokenOwnerSafe);
}
if (acl.hasRole(DEFAULT_ADMIN_ROLE, adminHolder)) {
revert UnexpectedDefaultAdmin(authoriser, adminHolder);
}
if (acl.hasRole(DEFAULT_ADMIN_ROLE, GRANTEE_SERVICE_1C66)) {
revert UnexpectedDefaultAdmin(authoriser, GRANTEE_SERVICE_1C66);
}
if (acl.hasRole(DEFAULT_ADMIN_ROLE, GRANTEE_SERVICE_3D0C)) {
revert UnexpectedDefaultAdmin(authoriser, GRANTEE_SERVICE_3D0C);
}
RoleGrant[] memory grants = expectedGrants(tokenOwnerSafe);
RoleGrant[] memory grants = expectedGrants(tokenOwnerSafe, adminHolder);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
for (uint256 i = 0; i < grants.length; i++) {
if (!acl.hasRole(grants[i].role, grants[i].grantee)) {
revert ExpectedGrantMissing(authoriser, grants[i].role, grants[i].grantee);
Expand Down
Loading
Loading