Guardian
The guardian is an emergency brake. It is a wallet that holds GUARDIAN_ROLE on the IssuerRegistry and can act at once, without the admin's delay, in two ways only: pause the system, and suspend an issuer. It exists because the admin is a multi-signature Safe (and, after the handover, a timelock with a delay), which is deliberately slow. A guardian key cannot register anyone, lift anything it did, or change roles, so stealing it allows a denial of service and nothing more.
How the app recognises a guardian
| Where | Check |
|---|---|
| Contract | IssuerRegistry.isGuardian(account), which is `hasRole(GUARDIAN_ROLE, account) |
| App | IssuerAccessService.accessOf(address).isAdmin, which is the same isGuardian read on v0.3. A wallet for which it is true gets the Admin workspace and sign-in sends it to /admin. |
/admin page | The Emergency pause panel appears only when the system is running and isGuardian is true for the connected wallet. |
The admin grants and revokes the role (grantRole(GUARDIAN_ROLE, account), revokeRole). The deploy script sets the first guardian from GUARDIAN_ADDRESS; when a new set joins an existing issuer registry, that variable is ignored because the shared registry keeps its own guardians.
Because the admin is also a guardian in the contract's eyes, everything on this page can be done by the admin too. In practice the admin is a Safe and does not browse the site, so the person who reaches /admin is the guardian.
What a guardian sees
/admin on v0.3 is the Governance page ("Who holds the admin keys, what is waiting to run, and the emergency brake. Everything here is read from the chain."). It is read-only apart from one button.
| Part | Content |
|---|---|
| System state | Running ("The system is running." "Issuers can issue new credentials.") or Paused ("The system is paused." "Only the admin can restart it, from the Safe.") |
| Admin facts | Admin (address), Admin transfer delay, Pending admin transfer (with the time it can be accepted, or "No admin transfer is pending.") |
| Timelock | In charge or Handover pending, and Queued admin actions each marked Ready or Waiting |
| Retired issuer keys | Each retired key and the key In favour of |
| Emergency pause | "Stops new credentials from being issued until the admin restarts the system. Credentials already issued can still be checked." and the button Pause the system |
| Admin actions | A note that adding or removing an issuer, restarting after a pause, lifting an issuer suspension and accepting an admin transfer run from the Safe with the Transaction Builder |
If the chain cannot be read, the page says "Could not read the governance state." A wallet that is not a guardian (or admin) is sent to /login.
Actions
Pause the system
- Connect the guardian wallet and open
/admin(or Admin in the top bar). - Press Pause the system and confirm the transaction in the wallet. The button reads "Waiting for wallet..." until it is mined.
- The state changes to Paused. Every page that creates something shows: "The system is paused. New credentials, invitations and record types cannot be created right now. Revoking, suspending and other corrections still work."
The pause is one flag on the IssuerRegistry; the CredentialRegistry, HolderRegistry, RecordTypeRegistry and the access gate read it from there. Calling pause() when already paused reverts (OpenZeppelin Pausable), which is why the panel is hidden while paused. Flow: Governance.
Suspend an issuer
There is no button for this. The guardian calls StatusRegistry.setIssuerSuspended(issuer, true, reason) directly (for example from the block explorer's write tab or a script). From then on StatusRegistry.isValid is false for every credential of that issuer (resolved through currentIssuerOf, so a successor key is covered too), and the verify page shows them as not valid. The issuer can still revoke its credentials.
Each contract set has its own StatusRegistry, so a suspension applies to the set whose registry was called. Use it when an issuer key may be stolen. Flow: Governance, Lifecycle.
Unpause (version dependent)
| Contract version of the set | Who may call unpause() |
|---|---|
| 0.3.0 (the set active on Sepolia as of 2026-10-24) | The admin only (onlyRole(DEFAULT_ADMIN_ROLE)) |
| 0.5.0 and later (revision R8) | Any guardian, or the admin |
The app does not offer an unpause button on either version; the page text says only the admin restarts. On a 0.5.0 set a guardian can still call unpause() directly. The runbook docs/process/runbooks/admin-keys.md describes the 0.3.0 behaviour.
What a guardian cannot do
- Add, remove, rename or retire issuers. Those are
onlyRole(DEFAULT_ADMIN_ROLE). - Lift an issuer suspension.
setIssuerSuspended(issuer, false, ...)checksDEFAULT_ADMIN_ROLEon theStatusRegistry. - Touch a single credential. Suspending one credential as an admin hold needs the
StatusRegistryadmin role; a guardian is not an issuer of anything. - Grant or revoke roles, including its own, or start an admin transfer.
- Unpause a 0.3.0 set.
- Stop verification or corrections. Reads, revocations and the other corrections work while paused.
Keys a guardian holds
One wallet, holding no funds beyond gas. The keys runbook lists it: if it is lost, the admin grants another guardian; if it may be stolen, the admin revokes it. When a stolen guardian key is suspected, the Safe restarts in one batch: revoke the guardian, unpause, then lift each wrongful issuer suspension, so the key cannot pause again between the steps.
The monitor (when configured) watches Paused, Unpaused, RoleGranted and RoleRevoked on the IssuerRegistry and IssuerSuspensionChanged on the StatusRegistry, so every guardian action raises an alert. See Monitoring.
Checklist
- Keep the guardian wallet separate from issuer and Safe owner keys.
- Know how to reach
/adminand confirm Pause the system works on the test network before you need it. - Pause first when issuance must stop; suspend an issuer when its credentials must read as not valid.
- Tell the Safe owners at once: only they can lift what you did (and restart a 0.3.0 set).
- Record the time, transaction and reason for the incident log.
Source files
blockchain/contracts/v03/IssuerRegistry.sol(isGuardian,pause,unpause,GUARDIAN_ROLE)blockchain/contracts/v03/StatusRegistry.sol(setIssuerSuspended,isValid)src/app/admin/GovernancePage.tsx,GovernancePanel.tsx,TimelockSection.tsx,RetiredKeysSection.tsxsrc/services/domain/platform-governance-service.ts,src/services/domain/issuer-access-service.tssrc/components/PausedNotice.tsx,src/services/react/use-platform-paused.tssrc/lib/server/ops/checks/chain-events.ts
Tests
- Contracts:
blockchain/test/v03/PauseClassification.test.ts,Governance.test.ts(including "lets a guardian pause and unpause, and a stranger neither"),SafeGovernance.test.ts,IssuerRegistry.test.ts,StatusRegistry.test.ts - Rendered:
src/app/admin/GovernancePanel.test.tsx - E2E:
e2e/journeys/pause-and-restart.journey.ts(a guardian pauses from/admin; issuing stops, verification and revocation keep working; the admin restarts)