Admin
The admin is whoever holds DEFAULT_ADMIN_ROLE on a contract set's IssuerRegistry and StatusRegistry. It decides which institutions may issue, restarts the system after a pause, lifts issuer suspensions, can place a hold on a single credential, and manages the guardian role. It is the most powerful role in the system, so it is held by a multi-signature wallet and, once the handover is finished, exercised only through a timelock that makes every change visible before it runs.
Words used here:
- Safe: a multi-signature smart account. On Sepolia the operator's Safe needs 2 of its 3 owners to sign.
- Timelock:
CertichainTimelock, an OpenZeppelinTimelockController. The Safe is its proposer and canceller, anyone may execute, and it has no admin of its own. An action is scheduled, waits the minimum delay, then runs. - Delayed admin transfer: both registries use OpenZeppelin
AccessControlDefaultAdminRules. A new admin is nominated withbeginDefaultAdminTransfer, waits the admin delay, then accepts withacceptDefaultAdminTransfer.
See Access control and governance and the Timelock contract page.
Who holds it (as of 2026-10-24)
| Contract set | Admin of IssuerRegistry and StatusRegistry | Notes |
|---|---|---|
| 0.3.0 (active on Sepolia) | The Safe, which accepted the role from the deployer on 2026-10-17 | Admin delay 3 days. The timelock was nominated, with acceptance possible from 2026-10-20; the handbook's last recorded status leaves the accept open. |
| 0.5.0 (deployed on Sepolia, not active) | The timelock | Taken over in the 2026-10-17 rehearsal; timelock delay 3600 s on Sepolia (2 days planned for production). |
| Local chain | The first Hardhat account (the deployer) | Tests and E2E use it directly. |
Read the live answer with cd blockchain && npm run governance:v03:sepolia (read only) or on /admin.
How the app recognises the admin
On v0.3, IssuerRegistry.isGuardian(address) is true for the admin, so accessOf(...).isAdmin is true and the Admin workspace appears. In practice the admin never uses it: a Safe or a timelock is a contract and cannot connect to the site as a browser wallet (the site has no WalletConnect support). The /admin page is a read-only view of the admin's state for everyone with the guardian role, and it sends no admin transaction. See Guardian for what the page shows.
What the admin controls
| Action | Contract and function | Notes |
|---|---|---|
| Register an institution | IssuerRegistry.addIssuer(issuer, name) | Name unique, 1 to 128 bytes; a retired key cannot be re-added |
| Remove an institution | IssuerRegistry.removeIssuer(issuer) | Its credentials stay on chain and StatusRegistry.isValid does not change; the VC, disclosure and API checks then report the issuer as not registered (the lookup page does not check registration) |
| Rename | IssuerRegistry.setIssuerName(issuer, name) | |
| Retire an issuer key | IssuerRegistry.retireIssuer(retired, successor) | Contract version 0.5.0 onward. The successor must be an active issuer; a lineage is at most 8 keys long |
| Restart after a pause | IssuerRegistry.unpause() | On 0.5.0 sets a guardian may also unpause |
| Pause | IssuerRegistry.pause() | The admin counts as a guardian |
| Grant or revoke the guardian role | IssuerRegistry.grantRole or revokeRole with GUARDIAN_ROLE() | |
| Suspend an issuer, or lift a suspension | StatusRegistry.setIssuerSuspended(issuer, suspended, reason) | Suspending is open to guardians; lifting is admin only |
| Hold a single credential | StatusRegistry.suspend(id, reason, noteHash) called by the admin | Sets adminHold; the issuer can then only revoke, not reinstate or supersede |
| Lift a hold | StatusRegistry.reinstate(id, noteHash) | Only the admin can lift its own hold |
| Hand the role on | beginDefaultAdminTransfer, acceptDefaultAdminTransfer (by the nominee), cancelDefaultAdminTransfer, changeDefaultAdminDelay | Every change waits for the admin delay |
| Wire a new set once | StatusRegistry.bindCredentialRegistry(registry) | Done at deployment; refuses a second binding |
The VersionAnnouncer (which announces new contract sets) has its own default admin and is not moved by the timelock handover script. See Version announcer and Contract set upgrade.
How an admin action is carried out
Before the handover: the Safe calls the registry
Two Safe owners build the call in the Safe's Transaction Builder (or import a batch file a script wrote), read the decoded call, sign, and execute. The runbook docs/process/runbooks/admin-keys.md lists the everyday calls.
After the handover: schedule, wait, execute
The proposal script reads the timelock's minimum delay and the active set from the manifest, and only writes files: TARGET=IssuerRegistry FUNCTION=addIssuer ARGS='["0x...","Institution name"]' npm run timelock:propose:sepolia. Targets are IssuerRegistry, StatusRegistry (of the active set, or SET=<label>) and Timelock. Admitting an institution under the draft consortium charter also passes SIGNOFF_FILE, so the salt is the hash of the accreditor's sign-off, and npm run admission:check:sepolia confirms the match. Flow: Governance.
What the admin cannot do
- Issue, revoke, reinstate or supersede someone else's credential. Revoking and superseding check the issuer key in charge. The admin's only credential-level power is the hold.
- Act instantly on a transfer. Changing who is admin, and changing the admin delay, wait for the delay. After the handover, every admin action waits for the timelock delay too.
- Rewrite history. Anchors, signatures and status events are permanent; removing an issuer does not delete its credentials.
- Read personal data. The chain holds none.
- Use the site's buttons. Admin actions are prepared as Safe batches; the governance screen is read only.
Keys behind the admin
| Key | Holder | Notes |
|---|---|---|
| Safe owner keys | Three named people | Two sign each batch. Losing one leaves two; losing two leaves the admin unable to act until a recovery described in the runbook. |
| Deployer key | The operator | Admin only until the Safe accepts; retired on Sepolia on 2026-10-17. |
The monitor (when configured) reads the admin and pending admin of every watched registry and warns when a transfer is pending, when registries disagree, or when a registry is not yet administered by the recorded timelock. It also watches CallScheduled, CallExecuted and Cancelled on the timelock. See Monitoring.
Checklist
- Before signing in the Safe, compare the decoded call with what the proposal script printed.
- Keep the salt the script prints; it is needed to execute.
- After scheduling, check
/adminshows the operation as Waiting, and later Ready. - Restart after a pause in the order the incident runbook gives (revoke a suspected guardian first, then unpause, then lift suspensions), in one batch.
- Record every admission and removal in the governance log; run
npm run governance:log:check. - Never sign an admin change that was not announced to the institutions in advance, unless it is an emergency.
Source files
blockchain/contracts/v03/IssuerRegistry.sol,blockchain/contracts/v03/StatusRegistry.sol,blockchain/contracts/v03/CertichainTimelock.sol,blockchain/contracts/v03/VersionAnnouncer.solblockchain/scripts/handover-v03.ts,governance-status.ts,deploy-timelock.ts,nominate-timelock.ts,timelock-handover.ts,timelock-propose.ts,admission-check.ts,governance-log-check.tsblockchain/scripts/lib/timelock-proposal.ts,blockchain/scripts/lib/timelock-handover.tssrc/app/admin/GovernancePage.tsx,src/app/admin/GovernancePanel.tsx,src/app/admin/TimelockSection.tsxsrc/services/domain/platform-governance-service.tssrc/lib/server/ops/checks/governance.ts,src/lib/server/ops/checks/chain-events.ts
Tests
- Contracts:
blockchain/test/v03/Governance.test.ts,SafeGovernance.test.ts,Handover.test.ts,Timelock.test.ts,TimelockDeploy.test.ts,TimelockHandover.test.ts,TimelockProposal.test.ts,TimelockTargets.test.ts,KeyLineage.test.ts,GovernanceLog.test.ts - Server:
src/lib/server/ops/checks/governance.test.ts - Rendered:
src/app/admin/GovernancePanel.test.tsx - E2E:
e2e/journeys/pause-and-restart.journey.ts(the admin restart is done by the harness helperunpauseAsAdmin),e2e/journeys/key-retirement.journey.ts(the harness helperreplaceIssuerKeyplays the admin)