Access control and governance
A credential registry is only trustworthy if the power to change it is limited and visible. Certichain's contracts decide who may register an institution, who may pause issuance and who may change a credential's status, and they make the most powerful actions slow and shared. This page explains the OpenZeppelin building blocks and the governance arrangement on Sepolia.
Roles with OpenZeppelin AccessControl
OpenZeppelin publishes audited, widely used Solidity libraries. Its AccessControl contract implements role-based access control:
- A role is a
bytes32name, usually the keccak-256 of a string. hasRole(role, account)answers whether an account holds a role; functions are guarded withonlyRole(role).- Each role has an admin role that may grant and revoke it. By default that is
DEFAULT_ADMIN_ROLE(the zero value). - Changes emit
RoleGrantedandRoleRevokedevents, so the history is public.
The v0.3 contracts use OpenZeppelin 5 (imported as @openzeppelin/contracts-v5); the legacy v0.2 contract uses OpenZeppelin 4.9 AccessControl (see Legacy v0.2).
AccessControlDefaultAdminRules
Plain AccessControl lets an admin hand the admin role to anyone in one step, and lets several accounts be admin at once. A typo or a stolen key would then be fatal. AccessControlDefaultAdminRules tightens this:
- There is exactly one default admin (
defaultAdmin()). - Moving it takes two steps with a delay: the current admin calls
beginDefaultAdminTransfer(newAdmin), and only after the admin delay has passed can the new admin callacceptDefaultAdminTransfer(). Until then the current admin can cancel. - Changing the delay itself is also delayed.
DEFAULT_ADMIN_ROLEcannot be granted withgrantRole.
In Certichain
| Contract | Uses | Admin powers |
|---|---|---|
IssuerRegistry | AccessControlDefaultAdminRules, Pausable | Add, remove, rename and (0.5.0) retire issuers; grant and revoke the guardian role; pause and unpause |
StatusRegistry | AccessControlDefaultAdminRules | Bind the credential registry (once, at deployment); place and lift an administrative hold on a credential; suspend an issuer and lift that suspension |
VersionAnnouncer | AccessControlDefaultAdminRules | Announce contract sets |
CredentialRegistry, HolderRegistry, RecordTypeRegistry | No admin | Rules only; they ask the IssuerRegistry who is an issuer and whether the system is paused |
The deploy scripts default to an admin delay of 3 days (DEFAULT_ADMIN_DELAY_SEC in blockchain/scripts/lib/deploy-v03.ts), overridable with ADMIN_DELAY_SEC. The 0.5.0 rehearsal set on Sepolia used 3,600 seconds.
Issuers are registrations, not roles
An institution is not given an AccessControl role. It is an address the admin has registered with a name in the IssuerRegistry:
| Function | Checks |
|---|---|
addIssuer(issuer, name) | Admin only; non-zero address; not already registered; not a retired key; name non-empty, at most 128 bytes (MAX_NAME_BYTES) and not used by another issuer |
removeIssuer(issuer) | Admin only; must be registered. Frees the name. Credentials already issued stay on chain |
setIssuerName(issuer, name) | Admin only; same name rules |
retireIssuer(retired, successor) | Admin only, contract version 0.5.0. The successor must be an active issuer; the chain of retired keys is capped at 8 hops (MAX_LINEAGE_HOPS) |
isIssuer is true for any registered address; isActiveIssuer additionally requires that the key has not been retired. Unique names stop one institution from posing as another under the same display name. Key retirement lets an institution move to a new key while its earlier credentials stay valid and are managed by the successor key (Key retirement).
Pausable and the guardian
OpenZeppelin's Pausable adds a paused() flag and _pause()/_unpause() functions. A guardian is a separate, lower-power role that can stop the system quickly without being able to change who is trusted.
bytes32 public constant GUARDIAN_ROLE = keccak256("CERTICHAIN_GUARDIAN");
function isGuardian(address account) public view returns (bool) {
return hasRole(GUARDIAN_ROLE, account) || hasRole(DEFAULT_ADMIN_ROLE, account);
}
What the pause stops
The flag lives in the IssuerRegistry, and the other contracts read it. The pause stops creating, not managing:
| Stopped while paused | Still allowed while paused |
|---|---|
Issuing credentials (CredentialRegistry reverts with SystemPaused) | Suspending, reinstating, revoking and superseding credentials |
Binding and rebinding holder wallets (HolderRegistry) | Reading and verifying anything |
Registering record types (RecordTypeRegistry) | Admin changes to the issuer list |
| Admitting members to, and proving with, the access gate |
An institution can still revoke a credential during an emergency, which is usually exactly when it needs to.
Who may do what
| Action | Guardian | Admin | Issuer |
|---|---|---|---|
pause() | Yes | Yes | No |
unpause() | Contract version 0.5.0 only | Yes | No |
Suspend a whole issuer (setIssuerSuspended(issuer, true, reason) in the StatusRegistry) | Yes | Yes | No |
| Lift an issuer suspension | No | Yes | No |
| Suspend or reinstate one credential | No | Yes, as an administrative hold that only the admin can lift | Yes, its own credentials |
| Revoke or supersede one credential | No | No | Yes, its own credentials (the key currently in charge) |
| Register issuers, grant the guardian role | No | Yes | No |
In contract version 0.3.0, which is the active set on Sepolia, only the admin can unpause. Version 0.5.0 lets the guardian unpause too (revision R8 in the handbook), so recovering from a false alarm does not need a multisig round. See Guardian.
Safe multisig wallets
A Safe (formerly Gnosis Safe) is a widely used smart contract wallet that executes a transaction only when a threshold of its owners has signed it, for example 2 of 3. Because a Safe is a contract, it signs messages through ERC-1271 (see Signatures). Owners collect signatures off chain in the Safe interface, and one of them submits the final transaction.
In Certichain
- On Sepolia the admin is a Safe 1.5.0 with 2-of-3 owners. It accepted the admin role of both admin-bearing registries of set 0.3.0 (
IssuerRegistryandStatusRegistry) on 2026-10-17, after the 3-day transfer delay. - The repository proves the transfer against a real Safe in tests (
blockchain/test/v03/SafeGovernance.test.ts) and provides handover and status scripts (governance:v03:sepoliareads the state without sending anything). - Admin actions are prepared as Safe transaction batch files (JSON) so owners can review them in the Safe interface; see Governance.
TimelockController
OpenZeppelin's TimelockController is a contract that executes calls only after a minimum delay. Proposers schedule an operation (target, value, call data, predecessor, salt, delay); after the delay, an executor executes it; a canceller can cancel it while it waits. Everyone can watch the queue on chain and react before a change takes effect, for example by removing their trust in a registry.
blockchain/contracts/v03/CertichainTimelock.sol is the OpenZeppelin timelock with no changes, named so its artefact is built with the other contracts. It is deployed with:
| Setting | Value |
|---|---|
| Proposer | The Safe only |
| Canceller | The Safe |
| Executor | The zero address, meaning anyone may run a ready operation |
| Admin | None, so the timelock administers itself |
| Minimum delay | 3,600 seconds on Sepolia (rehearsal); the handbook plans 2 days for production |
State on Sepolia as of 2026-10-24
From the handbook (ADR-0092 and the progress tracker):
- The timelock is deployed and verified on Sepolia. It is the admin of both admin-bearing registries of set 0.5.0, which is deployed but not the active set. Issuers were added and one key retired through scheduled Safe batches in a rehearsal.
- For set 0.3.0 (the active set), the Safe nominated the timelock as the next admin on 2026-10-17; the 3-day delay ended on 2026-10-20 at 08:42 UTC, after which the Safe can schedule the acceptance. The handbook does not record that acceptance as done, so the Safe should be treated as the direct admin of set 0.3.0 until
governance:v03:sepoliashows otherwise.
Governance beyond the contracts
A draft consortium charter (docs/governance/ in the handbook) describes how institutions would be admitted in production: an accreditor signs off, the sign-off file's hash becomes the timelock salt of the admission proposal (SIGNOFF_FILE on the proposal script), and admission:check:sepolia and governance:log:check let anyone check that each admission matches a recorded sign-off. The charter text is still under review; see Governance and Admin.
Source files
blockchain/contracts/v03/IssuerRegistry.sol,StatusRegistry.sol,VersionAnnouncer.sol,CertichainTimelock.solblockchain/contracts/v03/CredentialRegistry.sol,HolderRegistry.sol,RecordTypeRegistry.sol(pause checks)blockchain/contracts/zk/CertichainAccessGate.sol(pause checks)blockchain/scripts/lib/deploy-v03.ts,blockchain/scripts/lib/governance.tsblockchain/scripts/deploy-timelock.ts,nominate-timelock.ts,timelock-propose.ts,timelock-handover.ts,verify-timelock.tssrc/services/domain/governance-service.ts,src/services/domain/platform-governance-service.ts
Tests
blockchain/test/v03/IssuerRegistry.test.ts,StatusRegistry.test.ts,Governance.test.ts,PauseClassification.test.ts,KeyLineage.test.ts,Revisions.test.tsblockchain/test/v03/Handover.test.ts,SafeGovernance.test.tsblockchain/test/v03/Timelock.test.ts,TimelockDeploy.test.ts,TimelockHandover.test.ts,TimelockProposal.test.ts,TimelockTargets.test.tsblockchain/test/v03/Admission.test.ts,GovernanceLog.test.tssrc/services/domain/governance-service.test.ts,platform-governance-service.test.ts- E2E journeys
e2e/journeys/pause-and-restart.journey.ts,key-retirement.journey.ts