Skip to content

feat(authoriser): additional service signer — canonical pin + provisioning - #280

Merged
hardyjosh merged 6 commits into
mainfrom
ops/minter-service-wallet
Aug 10, 2026
Merged

feat(authoriser): additional service signer — canonical pin + provisioning#280
hardyjosh merged 6 commits into
mainfrom
ops/minter-service-wallet

Conversation

@hardyjosh

@hardyjosh hardyjosh commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

A second service EOA — GRANTEE_SERVICE_3D0C (0x3d0CD66EFA66c05d86c3d4316B03eAE87ab9E8aE) — joins GRANTEE_SERVICE_1C66 as an additional signer with the same three action roles (DEPOSIT / WITHDRAW / CERTIFY). Both signers active; nothing revoked.

Current-state invariants (per review — no time-sliced side lists). The signer's rows go straight into the canonical expectedGrants() map (11 → 14). Every consumer enforces the provisioning per chain — the cross-chain parity authoriser leg, the multichain production-state bundle, and the per-chain prod pins are RED on a chain until its bundle executes, then drift-guarded.

Consequences handled on their own terms:

  • assertAll() repointed to the current production authoriser — the V4 clone (codehash pin + grant map). The retired V3 authoriser is no longer asserted: asserting it against the current map would demand grants nobody will provision on a retired contract; a frozen V3 list would be a time slice. Its constant survives only as the migration-window pre value.
  • 20260619 MIRROR_COUNT 6 → 9 — the map grew and the script's own slice guard (GrantsSliceOutOfRange(5,6,14)) exists precisely to force this update; without it the script is un-dispatchable on any chain. Executed chains are unaffected (hydrated pins refuse re-runs); a future chain's ceremony provisions the full current map.
  • The pending swap-authoring suites mock the 3D0C rows to reach their subjects (scripts untouched). Sequencing: re-dispatching those authorings after this merges reverts on the 3D0C rows until provisioning executes on Base — execute the already-authored bundles as-is, or provision first.

Authoring. 20260723-provision-additional-service-signer (run-script, registered; network selector added): work items derived from the map's 3D0C rows, chain-aware, self-scoping, grants-only, n+1-proven reversible. Fork suite green on live Base + Ethereum.

Execute: run-script20260723-provision-additional-service-signer / run() / network base, sign; repeat ethereum. HyperEVM needs no bundle — its clone-deploy ceremony provisions the full current map.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VPs1hCTxusmaSeFKvoc4Kr

Summary by CodeRabbit

  • New Features

    • Added a script to provision an additional service signer with required role grants.
    • Added workflow support for running scripts on Base or Ethereum using a network selector.
  • Updates

    • Expanded authorisation coverage for the additional service signer.
    • Increased mirrored operational grants for authoriser clones.
  • Tests

    • Added coverage for complete, partial, and already-completed provisioning scenarios across both networks.
    • Removed obsolete authoriser swap tests.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@thedavidmeister, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 49 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 34a47b8a-3e50-4890-b66f-0e2142c2d7aa

📥 Commits

Reviewing files that changed from the base of the PR and between 16e2aaa and a8356f9.

📒 Files selected for processing (2)
  • script/20260723-provision-additional-service-signer.s.sol
  • test/script/20260723-provision-additional-service-signer.t.sol
📝 Walkthrough

Walkthrough

Adds a Safe-based script for provisioning an additional service signer on Base and Ethereum. It expands authoriser grant invariants, updates network-aware script dispatch, increases mirrored grants from six to nine, and adds fork and unit coverage.

Changes

Service signer provisioning

Layer / File(s) Summary
Authoriser grant-map updates
src/lib/LibAuthoriserInvariants.sol
Adds the additional signer and its DEPOSIT, WITHDRAW, and CERTIFY grants, updates production authoriser pinning, and extends admin-role assertions.
Safe provisioning flow
script/20260723-provision-additional-service-signer.s.sol
Validates chain and authoriser state, creates Safe grant transactions for missing roles, simulates execution, emits a transaction-builder artifact, and proves revoke/re-grant reversibility.
Chain-aware dispatch and fork tests
.github/workflows/run-script.yaml, test/script/20260723-provision-additional-service-signer.prod.t.sol
Adds Base/Ethereum workflow selection and verifies the script exits when the signer is already provisioned on both chains.
Harness and local bundle tests
test/script/ProvisionAdditionalServiceSignerHarness.sol, test/script/20260723-provision-additional-service-signer.t.sol
Exposes the bundle builder and tests missing grants, partial provisioning, idempotence, and grant-map drift.
Authoriser clone mirror alignment
script/20260619-deploy-v4-authoriser-clone.s.sol, test/script/20260619-deploy-v4-authoriser-clone.t.sol
Updates the mirrored operational-grant count and regression assertion from six to nine.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Dispatcher
  participant ProvisionAdditionalServiceSigner
  participant Safe
  participant Authoriser
  Dispatcher->>ProvisionAdditionalServiceSigner: select network and run script
  ProvisionAdditionalServiceSigner->>Authoriser: validate clone and missing grants
  ProvisionAdditionalServiceSigner->>Safe: simulate grantRole bundle
  Safe->>Authoriser: apply missing role grants
  ProvisionAdditionalServiceSigner->>Safe: simulate revoke and re-grant proof
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: thedavidmeister, alastairong1

Poem

A rabbit packed grants in a Safe-bound tray,
Three signer roles hopped into play.
Base and Ethereum followed the route,
Missing pairs found what they were about.
Revoke, re-grant, then hop away. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: adding the service signer, pinning the canonical authoriser, and adding provisioning.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ops/minter-service-wallet

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hardyjosh
hardyjosh force-pushed the ops/minter-service-wallet branch from 9d5c95a to 0949e8a Compare July 23, 2026 14:35
@hardyjosh hardyjosh changed the title ops(script): provision the mint/burn service wallet on the V4 authoriser feat(authoriser): service-signer straight swap — pin + authoring Jul 23, 2026
@hardyjosh
hardyjosh force-pushed the ops/minter-service-wallet branch from 0949e8a to f3c68e9 Compare July 23, 2026 14:42
@hardyjosh hardyjosh changed the title feat(authoriser): service-signer straight swap — pin + authoring feat(authoriser): additional service signer — canonical pin + provisioning Jul 23, 2026
@hardyjosh
hardyjosh force-pushed the ops/minter-service-wallet branch 5 times, most recently from b215cce to 4663984 Compare July 23, 2026 15:22
@hardyjosh
hardyjosh marked this pull request as ready for review July 23, 2026 15:27
@hardyjosh
hardyjosh force-pushed the ops/minter-service-wallet branch from 4663984 to d168ee1 Compare July 23, 2026 15:31

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@script/20260619-deploy-v4-authoriser-clone.s.sol`:
- Around line 127-135: Update the Step 2 and post-state comments near
MIRROR_START_INDEX and MIRROR_COUNT in
script/20260619-deploy-v4-authoriser-clone.s.sol to describe nine grants instead
of six. Also update the happy-path and replica comments near line 335 in
test/script/20260619-deploy-v4-authoriser-clone.t.sol to say nine grants, with
no code behavior changes.

In `@script/20260723-provision-additional-service-signer.s.sol`:
- Around line 153-157: Update the additional-role validation loop to first
assert that roleAdminOf(additional[i].role) matches
acl.getRoleAdmin(additional[i].role), before checking acl.hasRole for safeAddr.
Preserve the existing Safe admin-grant validation and revert behavior after this
live role-admin mapping check.

In `@src/lib/LibAuthoriserInvariants.sol`:
- Around line 63-74: The current V4 authoriser pin is incorrectly reused for
migration; add a distinct retired V3 predecessor constant in
LibAuthoriserInvariants while retaining STOX_PROD_AUTHORISER for assertAll(). In
src/lib/LibAuthoriserInvariants.sol lines 63-74, define and use the V3 pin for
migration. In src/lib/LibInvariants.sol lines 39-40, update the migration-window
documentation and call path to reference that separate V3 predecessor.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7bb8c455-3d76-4196-9880-d7831315e852

📥 Commits

Reviewing files that changed from the base of the PR and between 791b917 and 4663984.

📒 Files selected for processing (9)
  • .github/workflows/run-script.yaml
  • script/20260619-deploy-v4-authoriser-clone.s.sol
  • script/20260723-provision-additional-service-signer.s.sol
  • src/lib/LibAuthoriserInvariants.sol
  • src/lib/LibInvariants.sol
  • test/script/20260619-deploy-v4-authoriser-clone.t.sol
  • test/script/20260722-swap-remaining-vault-authorisers.t.sol
  • test/script/20260722-swap-rklb-authoriser.t.sol
  • test/script/20260723-provision-additional-service-signer.t.sol
💤 Files with no reviewable changes (2)
  • test/script/20260722-swap-remaining-vault-authorisers.t.sol
  • test/script/20260722-swap-rklb-authoriser.t.sol

Comment thread script/20260619-deploy-v4-authoriser-clone.s.sol Outdated
Comment thread script/20260723-provision-additional-service-signer.s.sol
Comment thread src/lib/LibAuthoriserInvariants.sol Outdated
…ning, post-swap test retirement

Rebased onto the #254 grant-map restructure (13-entry parametric form,
STOX_PROD_AUTHORISER repointed to the live authoriser upstream).

GRANTEE_SERVICE_3D0C joins GRANTEE_SERVICE_1C66 as an ADDITIONAL service
signer with the same three action roles:

- expectedGrants() 13 -> 16; every consumer (parity authoriser leg,
  production-state bundle, per-chain prod pins) is red per chain until the
  provisioning bundle executes there, drift-guarded after. 3D0C joins the
  DEFAULT_ADMIN absence checks. Map docs state purpose, not contents.
- 20260619 clone-deploy MIRROR_COUNT 6 -> 9 (+ suite): the ceremony runs
  again on future chains and its slice guard forces the constant to track
  the canonical map; executed chains refuse re-runs via hydrated pins.
- 20260723-provision-additional-service-signer (run-script, registered):
  authors the per-chain grant bundle, work items derived from the map,
  chain-aware (base/ethereum network selector added to the dispatcher),
  self-scoping, n+1-proven reversible. Fork suite green on live Base +
  Ethereum.
- Post-execution retirement: the executed swap authorings' one-shot test
  suites deleted; live-state coverage is carried by the standing
  invariants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPs1hCTxusmaSeFKvoc4Kr
thedavidmeister and others added 3 commits August 5, 2026 09:27
The fork suite reads live Base and Ethereum. Production moved after the last
run (2026-07-29) when the additional service signer was signed on mainnet, so
that run's result describes a state that no longer exists.
…ocks

The provisioning selection was only reachable through `run()`, which reads
`block.chainid` and the pinned production Safe + authoriser, so every
scenario about WHICH pairs get authored had to be a fork test with
`vm.mockCall` standing in for the role state. A mocked fork fixture is a
premise the next legitimate operation deletes: signing the bundle on Base
made two of those tests fail, because the script was right and the fixture
was stale.

Extract the selection into `authorBundle(authoriser, safeAddr)` — same
pre-flight, same self-scoping, same reverts, every chain-specific input
now an argument — and drive it from a harness against a real EIP-1167
clone of the real authoriser whose role state is built by real `grantRole`
calls under the Safe's `_ADMIN` roles. `run()` calls the same function, so
the logic under test is the logic that ships.

Scenarios covered without a fork or a mock: nothing provisioned authors
all three pairs; CERTIFY held authors only DEPOSIT/WITHDRAW; DEPOSIT held
authors only WITHDRAW/CERTIFY (the bundle is not a prefix of the map);
every pair held reverts `AdditionalSignerAlreadyProvisioned`; a missing
non-signer row reverts as drift. The last two were previously mock-only or
uncovered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A prod test must assert what production IS, not what production needed
when the PR was written. The two fork tests this replaces asserted the
latter — one drove `run()` to completion on Base and Ethereum, the other
mocked CERTIFY as held — and both were falsified the moment the Safe
signed the bundle, which is the correct outcome for that shape of test,
not a regression.

Measured directly on both chains: Base `0x315b16faa6eE413faBCa877d3851
B3818369f0cD` and Ethereum `0x66566cc91dEAf818859bD4b09B7903ac48998157`
each return true for DEPOSIT, WITHDRAW and CERTIFY on
`GRANTEE_SERVICE_3D0C`, with identical clone codehashes and no pinned
grantee holding `DEFAULT_ADMIN_ROLE`. The rollout is complete on every
chain carrying a live authoriser.

So the prod layer asserts that, unmocked, per chain: the signer holds
every canonical pair, and `run()` refuses to author an empty bundle
there. `run()` reaches that refusal only after its full pre-flight
passes, so each test also pins the chain's Safe, the authoriser pin and
codehash, and the rest of the grant map. A revoke on either chain turns
this red, which is exactly the drift signal wanted.

`testRunRevertsWhenAlreadyProvisioned` survives here with its
`vm.mockCall`s deleted — on a genuinely provisioned chain it needed none
— and now runs per chain rather than Base only.

Flips the script's status line to EXECUTED, verified against the chains
the prod tests read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@thedavidmeister

Copy link
Copy Markdown
Contributor

Provisioning tests split into a genuine unit layer and a genuine prod layer

The two red tests were red because the script is correct and their premise had expired: the bundle has been signed and executed, so run() correctly refuses to author an empty one. Fixed by splitting the file rather than by weakening either side.

Measured per-chain, per-role matrix

Read directly off both chains (hasRole on the pinned V4 clones), 2026-08-05:

chain authoriser DEPOSIT WITHDRAW CERTIFY clone runtime keccak
Base (8453) 0x315b16faa6eE413faBCa877d3851B3818369f0cD true true true 0x2089950d3cc1112dd66a58adcfadeadc490b50053ac67be8bc676b4a2dcd1717
Ethereum (1) 0x66566cc91dEAf818859bD4b09B7903ac48998157 true true true 0x2089950d3cc1112dd66a58adcfadeadc490b50053ac67be8bc676b4a2dcd1717

for GRANTEE_SERVICE_3D0C = 0x3d0CD66EFA66c05d86c3d4316B03eAE87ab9E8aE.

Ethereum is in the same state as Base, not a different one. The full 16-row expectedGrants() map holds on both — all seven _ADMIN roles on each chain's own token-owner Safe, the 1C66 service signer's three, the Safe's own three, and the 3D0C signer's three — and none of the three pinned grantees holds DEFAULT_ADMIN_ROLE on either chain. The rollout is complete on every chain carrying a live authoriser; HyperEVM has no authoriser yet.

The script's status line is flipped to **EXECUTED — verified 2026-08-05 (Base and Ethereum).** accordingly. It was still claiming **PENDING.**, which is the same class of staleness as the tests.

Where each scenario went

scenario before after
nothing provisioned -> authors all three pairs testRunCompletesOnBothLiveChains (fork) unit testAuthorsEveryPairWhenNothingIsProvisioned
partially provisioned -> authors only the missing testRunAuthorsOnlyTheMissingPairs (fork + vm.mockCall) unit testAuthorsOnlyTheMissingPairsWhenCertifyIsHeld and ...WhenDepositIsHeld
fully provisioned -> reverts AdditionalSignerAlreadyProvisioned testRunRevertsWhenAlreadyProvisioned (fork + 3x vm.mockCall) unit testRevertsWhenEveryPairIsAlreadyHeld and prod testBaseIsProvisioned / testEthereumIsProvisioned with the mocks deleted
grant map has drifted -> refuses to provision not covered unit testRevertsWhenTheGrantMapHasDrifted
live per-chain state Base only, and only as a side effect prod, per chain, from the repo's network enumeration

Net: 3 fork tests (2 of them mocked) -> 5 unit tests + 2 prod tests. No scenario lost; two gained.

Unit layer — no fork, no mocks

The selection was only reachable through run(), which reads block.chainid and the pinned production Safe and authoriser — which is why it could only be tested with mocks on a fork. Extracted as authorBundle(authoriser, safeAddr): same pre-flight, same self-scoping, same reverts, every chain-specific input now an argument. run() calls it, so the logic under test is the logic that ships.

The fixture is a real Clones.clone of the real StoxOffchainAssetReceiptVaultAuthorizerV1, initialised with a local Safe stand-in, driven into each state by real grantRole calls under the Safe's _ADMIN roles — the same code path production used. The non-signer rows are granted by iterating expectedGrants() itself, so the fixture cannot drift from the map the script reads. Nothing here can be invalidated by anything signed on-chain.

The bundle assertions check the authored calldata (grantRole(role, SIGNER), value 0, CALL) pair by pair, and ...WhenDepositIsHeld pins that the bundle tracks which pair is held rather than merely how many.

Prod layer — real forks, no mocks

test/script/20260723-provision-additional-service-signer.prod.t.sol, one test per chain: the signer holds every canonical pair, and run() refuses to author against it. run() reaches that refusal only after its full pre-flight passes, so each test also pins the chain's Safe, the authoriser pin and codehash, and the rest of the grant map. A revoke on either chain turns this red — the drift signal that a mocked fixture could never give.

What was deleted, and why it is safe

run()'s end-to-end walk on a live fork (SafeTxHash -> simulate -> post-state -> artifact -> n+1 reversal) is no longer reachable on any chain: both are provisioned, so run() always reverts in pre-flight, and the only ways to restore it would be a mock (the defect being removed) or an unprovisioned chain (none exists). Its parts keep their own coverage — LibSafeOps round-trip/simulate/n+1 in test/src/lib/LibSafeOps.t.sol, Safe invariants in test/src/lib/LibSafeInvariants.t.sol, and the post-state map per chain in StoxCrossChainParity.t.sol. The substantive part specific to this script — which pairs get authored — is what moved to the unit layer.

The shared out/ artifact path stops being a constraint on this file: no test in either layer reaches vm.writeFile, so the chains no longer have to be walked serially inside one test.

Two things worth a reviewer's eye

  1. LibAuthoriserInvariants has no deployer-holds-nothing assertion. Checked rather than assumed: the "deployer retains no _ADMIN" check (DeployerStillHoldsAdminRole) lives in script/20260619-deploy-v4-authoriser-clone.s.sol::_assertPostState, not in the lib. The lib's nearest equivalent is the negative UnexpectedDefaultAdmin assertion over the three pinned grantees inside assertExpectedGrants — and that already runs per chain in StoxCrossChainParity.t.sol (Base and Ethereum legs, both live today). So the prod sweep is covered and this file deliberately does not duplicate it.

  2. SafeMissingRoleAdmin is unreachable. roleAdminOf returns DEPOSIT_ADMIN / WITHDRAW_ADMIN / CERTIFY_ADMIN, and those three are rows 0-2 of expectedGrants(safe) — so the earlier drift require fires first on exactly the state that would raise it. Left in place (behaviour unchanged by this PR); flagging it rather than folding an error-surface change into a test split.

Verification

forge build clean. forge fmt --check, slither . (0 findings), reuse lint, rainix-sol-single-contract all clean locally via nix. New tests locally: 5/5 unit, 2/2 prod against real Base and Ethereum forks. Full-suite counts from CI, since local fork runs here are rate-limited by the RPC endpoints available to me rather than CI's.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@script/20260723-provision-additional-service-signer.s.sol`:
- Around line 46-49: Correct the HyperEVM claim in the script comments: since
activeChainAuthoriser does not support HyperEVM and reverts
UnsupportedChainForProvisioning, remove the assertion that HyperEVM is
dispatchable after bootstrap and state that this script supports only Base and
Ethereum, unless an actual HyperEVM branch is implemented.

In `@test/script/20260723-provision-additional-service-signer.t.sol`:
- Around line 110-116: Add a reusable helper in
test/script/20260723-provision-additional-service-signer.t.sol that verifies all
three canonical RoleGrant entries, including role, grantee, and ordering, then
invoke it for the returned additional array at lines 110-116, 126-131, and
142-146. At the final site, retain the additional result before asserting its
canonical contents; leave the existing transaction-selection assertions intact.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0341732b-e846-4c23-8e97-4d16ba7c10a9

📥 Commits

Reviewing files that changed from the base of the PR and between 11e5a91 and 16e2aaa.

📒 Files selected for processing (4)
  • script/20260723-provision-additional-service-signer.s.sol
  • test/script/20260723-provision-additional-service-signer.prod.t.sol
  • test/script/20260723-provision-additional-service-signer.t.sol
  • test/script/ProvisionAdditionalServiceSignerHarness.sol

Comment thread script/20260723-provision-additional-service-signer.s.sol Outdated
Comment thread test/script/20260723-provision-additional-service-signer.t.sol
Two fixes and one documented falsification.

HyperEVM: `activeChainAuthoriser()` has only Base and Ethereum branches
and reverts `UnsupportedChainForProvisioning` for anything else, so the
header claim that HyperEVM becomes dispatchable once its authoriser
bootstraps was wrong — it needs a branch here first. Say that instead.

Canonical pairs: the unit tests asserted `additional.length` but not its
contents, so a wrong role, wrong grantee or wrong order could pass. Add
`assertCanonicalPairs` and call it in all three selection scenarios; the
last one now retains `additional` rather than discarding it. Order is
load-bearing — it decides which pair drives the n+1 reversal walk.

Role-admin mapping: NOT reading `getRoleAdmin` live is deliberate, and
the reason now sits in `roleAdminOf`'s NatSpec rather than being implied.
The hierarchy is written by `_setRoleAdmin` inside `initialize` and
nowhere else — OpenZeppelin's `_setRoleAdmin` is `internal` and
`AccessControlUpgradeable` exposes no external setter — so an
implementation's mapping is fixed at initialisation and immutable after.
`activeChainAuthoriser()` already asserts the clone's runtime codehash
equals the pinned EIP-1167 runtime embedding the audited 0.1.1 impl,
which is what establishes WHICH mapping the clone carries. A live
`getRoleAdmin` read would re-derive that, and could never fail on a
clone that passed the codehash gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hardyjosh
hardyjosh merged commit 90c0933 into main Aug 10, 2026
33 of 34 checks passed
thedavidmeister pushed a commit that referenced this pull request Aug 11, 2026
The admin-holder parameterisation was written against a 13-entry map; the
multichain stack underneath now carries the three additional service-signer
rows from #280, so the merged map is 16. The structure test still asserted
13 and failed the restacked branch.

Widens it to 16 and asserts the new rows explicitly: the additional signer's
three action roles are OPERATIONAL, so they must track the signer regardless
of who holds the _ADMIN slice — the property that keeps the timelock
migration from ever moving them.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants