You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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(bytes32role, addressdeployer);
bytes32[AUTO_GRANTED_ADMIN_COUNT] memory adminRoles =autoGrantedAdminRoles();
for (uint256 i =0; i < adminRoles.length; i++) {
if (acl.hasRole(adminRoles[i], deployer)) {
revertDeployerStillHoldsAdminRole(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:
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.
#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.
What
LibAuthoriserInvariants.assertExpectedGrantsasserts only that the pinned(role, grantee)pairs are present. Its sole negative assertion isDEFAULT_ADMIN_ROLE, and only against two addresses — the token-owner Safe andGRANTEE_SERVICE_1C66.Nothing in the ongoing invariant asserts that any other address is absent from the
_ADMINroles. So if the deploy key — or anyone else — holdsDEPOSIT_ADMIN,WITHDRAW_ADMIN,CERTIFY_ADMIN,CONFISCATE_SHARES_ADMIN,CONFISCATE_RECEIPT_ADMIN,SCHEDULE_CORPORATE_ACTION_ADMINorCANCEL_CORPORATE_ACTION_ADMINon the production authoriser, every assertion in the lib still passes silently.That matters because an
_ADMINrole is root over its action role's grant map: a holder can grantDEPOSIT/WITHDRAW/CERTIFYto an arbitrary address.DEFAULT_ADMIN_ROLEis 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.solhas a dedicated typed error and a post-state loop over all seven roles: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-scheduledsweep — 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.solcurrently 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
_ADMINroles, run against the same map the positive leg uses. Two design questions worth settling first:AccessControlcannot 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.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.