Skip to content

Authoriser grant invariant is presence-only: a post-deploy _ADMIN re-grant is invisible #281

Description

@thedavidmeister

What

LibAuthoriserInvariants.assertExpectedGrants asserts only that the pinned (role, grantee) pairs are present. Its sole negative assertion is DEFAULT_ADMIN_ROLE, and only against two addresses — the token-owner Safe and GRANTEE_SERVICE_1C66.

Nothing in the ongoing invariant asserts that any other address is absent from the _ADMIN roles. So if the deploy key — or anyone else — holds DEPOSIT_ADMIN, WITHDRAW_ADMIN, CERTIFY_ADMIN, CONFISCATE_SHARES_ADMIN, CONFISCATE_RECEIPT_ADMIN, SCHEDULE_CORPORATE_ACTION_ADMIN or CANCEL_CORPORATE_ACTION_ADMIN on the production authoriser, every assertion in the lib still passes silently.

That matters because an _ADMIN role is root over its action role's grant map: a holder can grant DEPOSIT / WITHDRAW / CERTIFY to an arbitrary address. DEFAULT_ADMIN_ROLE is not the only escalation path, and it is the only one currently guarded.

This is a gap in the continuous invariant, not an unchecked claim

The deploy script already gets this right at deploy time. script/20260619-deploy-v4-authoriser-clone.s.sol has a dedicated typed error and a post-state loop over all seven roles:

error DeployerStillHoldsAdminRole(bytes32 role, address deployer);

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);
    }
}

with the comment: "closes the 'transitional trust window' for those specific roles."

But that runs once, when the script runs. The invariant lib is what runs continuously — in the prod-state fork tests and the daily rainix-sol-scheduled sweep — and it carries no equivalent. A role granted after the deploy, whether by mistake or by a compromised admin, is exactly the drift the scheduled suite exists to catch, and it is invisible to it.

LibAuthoriserInvariants.sol currently comments that the clone-deploy broadcast "renounced them from the deploy key". That is true of what the script asserted at the time; it is not an ongoing guarantee.

Suggested fix

Give the lib a negative leg over the seven _ADMIN roles, run against the same map the positive leg uses. Two design questions worth settling first:

  1. Which addresses to check. A plain AccessControl cannot enumerate members, so exhaustive absence isn't possible — the check has to be against pinned addresses. The deploy key is the obvious one and is not currently pinned in the lib.
  2. Where the role list lives. autoGrantedAdminRoles() is currently script-local. If the lib needs it too, it should move to the lib as the single source rather than being hand-listed twice — the same consolidation that feat(invariants): repoint STOX_PROD_AUTHORISER to the V4 clone — RED until swap #254 applied to the grant map itself.

Why not a blocker on #254

#254 (merged, 64a0d245) repointed the invariant to the live V4 clone and unified the grant map to 13 parametric entries. That was a strict improvement — before it, the lib asserted the retired V3 clone. This gap predates it and is orthogonal.

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions