Skip to main content

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 bytes32 name, usually the keccak-256 of a string.
  • hasRole(role, account) answers whether an account holds a role; functions are guarded with onlyRole(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 RoleGranted and RoleRevoked events, 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 call acceptDefaultAdminTransfer(). Until then the current admin can cancel.
  • Changing the delay itself is also delayed.
  • DEFAULT_ADMIN_ROLE cannot be granted with grantRole.

In Certichain​

ContractUsesAdmin powers
IssuerRegistryAccessControlDefaultAdminRules, PausableAdd, remove, rename and (0.5.0) retire issuers; grant and revoke the guardian role; pause and unpause
StatusRegistryAccessControlDefaultAdminRulesBind the credential registry (once, at deployment); place and lift an administrative hold on a credential; suspend an issuer and lift that suspension
VersionAnnouncerAccessControlDefaultAdminRulesAnnounce contract sets
CredentialRegistry, HolderRegistry, RecordTypeRegistryNo adminRules 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:

FunctionChecks
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 pausedStill 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​

ActionGuardianAdminIssuer
pause()YesYesNo
unpause()Contract version 0.5.0 onlyYesNo
Suspend a whole issuer (setIssuerSuspended(issuer, true, reason) in the StatusRegistry)YesYesNo
Lift an issuer suspensionNoYesNo
Suspend or reinstate one credentialNoYes, as an administrative hold that only the admin can liftYes, its own credentials
Revoke or supersede one credentialNoNoYes, its own credentials (the key currently in charge)
Register issuers, grant the guardian roleNoYesNo

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 (IssuerRegistry and StatusRegistry) 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:sepolia reads 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:

SettingValue
ProposerThe Safe only
CancellerThe Safe
ExecutorThe zero address, meaning anyone may run a ready operation
AdminNone, so the timelock administers itself
Minimum delay3,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:sepolia shows 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.sol
  • blockchain/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.ts
  • blockchain/scripts/deploy-timelock.ts, nominate-timelock.ts, timelock-propose.ts, timelock-handover.ts, verify-timelock.ts
  • src/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.ts
  • blockchain/test/v03/Handover.test.ts, SafeGovernance.test.ts
  • blockchain/test/v03/Timelock.test.ts, TimelockDeploy.test.ts, TimelockHandover.test.ts, TimelockProposal.test.ts, TimelockTargets.test.ts
  • blockchain/test/v03/Admission.test.ts, GovernanceLog.test.ts
  • src/services/domain/governance-service.test.ts, platform-governance-service.test.ts
  • E2E journeys e2e/journeys/pause-and-restart.journey.ts, key-retirement.journey.ts