Skip to main content

Smart contracts

Certichain keeps every fact that must be trusted without a server on an Ethereum-compatible chain: who may issue, what was issued, who signed it, and whether it still stands. This section documents each contract in blockchain/contracts/, function by function, with the checks it makes, the events it emits and the errors it raises.

All facts on these pages come from the Solidity sources and the Hardhat tests. Status statements are as of 2026-10-24.

The contracts at a glance​

ContractFolderRoleAdmin
IssuerRegistryblockchain/contracts/v03/Who may issue, their unique names, key retirement, the guardian role and the one pause flag of a setDefault admin with a delayed two-step transfer
CredentialRegistryblockchain/contracts/v03/Anchors credentials signed by an issuer (EIP-712), the single entry point of a setNone
StatusRegistryblockchain/contracts/v03/Status of each credential (active, suspended, revoked, superseded) and issuer-wide suspensionDefault admin with a delayed two-step transfer
HolderRegistryblockchain/contracts/v03/Binds a student wallet to an issuer's enrolment commitment through signed invitationsNone
RecordTypeRegistryblockchain/contracts/v03/Custom record types each issuer defines for itselfNone
CredentialTypesblockchain/contracts/v03/libraries/Shared structs and the status enumNot a contract instance
CertichainTimelockblockchain/contracts/v03/The OpenZeppelin timelock, run by the admin Safe, that delays admin actionsItself
VersionAnnouncerblockchain/contracts/v03/Append-only notice board of contract setsDefault admin
CertichainAccessGateblockchain/contracts/zk/Semaphore groups per credential, anonymous proofs of access, receiptsNone
AcademicRecords and modulesblockchain/contracts/The legacy v0.2 system (release v2.0.0)Super admin and admins

The five registries together are called a contract set. A deployment manifest lists one or more sets per chain; see Contract versions and sets and the Deployment manifest format.

How the contracts reference each other​

The access gate calls getCredential on the credential registry and isValid and holderOf on the status and holder registries it read from the credential registry at deployment; the diagram draws those three calls as one arrow.

Points that the diagram compresses:

  • CredentialRegistry holds immutable addresses of the other four registries, so a client that knows one credential registry address can discover the whole set through its getters (issuerRegistry(), statusRegistry(), holderRegistry(), recordTypeRegistry()).
  • StatusRegistry takes the issuer registry in its constructor and is linked to its credential registry once, after deployment, through bindCredentialRegistry. Only that credential registry may call initialise and supersedeFor.
  • The pause flag lives in IssuerRegistry only. The other contracts ask issuerRegistry.paused() before they create anything.
  • The access gate reads the issuer, status and holder registries from the credential registry it was deployed with, so one gate serves exactly one set.
  • CertichainTimelock is not referenced by any registry. It becomes the admin of IssuerRegistry and StatusRegistry through their two-step admin transfer.

Design rules that hold across the set​

RuleWhere it is enforced
No plaintext personal data on chain: subjects and metadata appear only as salted commitments and hashesData model in CredentialTypes.sol; logs and storage scan in blockchain/test/v03/NoPlaintext.test.ts
A credential is anchored only with the issuer's own EIP-712 signature, and the issuer sends the transactionCredentialRegistry._issue
Nothing is deleted. Status moves forward; revoked and superseded are terminalStatusRegistry transition checks; invariant suite
Pause stops creation only. Corrections (revoke, suspend, reinstate, supersede, unbind) keep workingblockchain/test/v03/PauseClassification.test.ts
No function loops over unbounded storage. Lists are rebuilt from eventsblockchain/test/v03/BoundedWork.test.ts
The only admin powers are issuer registration, guardian and pause control, issuer-wide suspension and admin holds. No admin can edit or revoke a credentialFunction tables on each page
Contracts are not upgradeable. A change ships as a new set beside the old oneContract set upgrade

Pages in this section​

PageWhat it covers
Credential typesThe shared structs, the status enum and the EIP-712 type strings
Issuer registryIssuers, names, key retirement and lineage, guardian, pause
Credential registryIssuance checks, credential ids, atomic re-issue
Status registryThe status machine, reasons, admin holds, issuer suspension, isValid
Holder registryInvitations, binding epochs, rebinding and unbinding
Record type registryBuilt-in, reserved and custom type ids
Access gateSemaphore groups per credential, scopes, epochs, nullifiers and receipts
TimelockThe admin timelock, its roles, delays and the handover
Version announcerThe advisory notice board of sets
Legacy v0.2AcademicRecords, RoleManager, RecordStorage, StudentManagement
Events and errorsEvery event and custom error in one reference
Gas and limitsGas snapshot figures, size budgets and every hard limit
Deployment and scriptsCompiler profiles, deploy, verify, governance and ZK scripts

Where the app talks to the contracts​

The web app never holds contract addresses in code. It reads them from the manifests in src/contracts/v03/deployment.<chainId>.json and src/contracts/zk/deployment.<chainId>.json, and the ABIs exported next to them. Reads go through a read-only RPC provider, so a verifier needs no wallet; writes go through the connected wallet. See Reading the chain and Contract sets routing.

App serviceContract calls
src/services/domain/credential-service.tsissue, issueReplacing, getCredential, contractVersion
src/services/domain/status-service.tssuspend, reinstate, revoke, supersede, supersedeToRegistry, statusOf, isValid, isIssuerSuspended
src/services/domain/enrolment-service.tsredeem, rebind, unbind, revokeInvitations, holderOf, enrolmentOf, epochOf
src/services/domain/record-type-service.tsregisterType, setTypeActive, typeOf
src/services/domain/platform-governance-service.tspause, paused, isGuardian, defaultAdmin, pendingDefaultAdmin, timelock reads
src/services/domain/access-service.ts and access-requester-service.tsadmit, removeMember, proveAccess, accessGroupOf, currentEpoch, scopeOf

Admin actions on the registries (addIssuer, removeIssuer, retireIssuer, setIssuerSuspended, unpause) are not sent from the web app. They go through the Safe and the timelock with the scripts in blockchain/scripts/ (see Deployment and scripts and Governance).

Source files​

  • blockchain/contracts/v03/IssuerRegistry.sol, CredentialRegistry.sol, StatusRegistry.sol, HolderRegistry.sol, RecordTypeRegistry.sol
  • blockchain/contracts/v03/libraries/CredentialTypes.sol and blockchain/contracts/v03/interfaces/
  • blockchain/contracts/v03/CertichainTimelock.sol, blockchain/contracts/v03/VersionAnnouncer.sol
  • blockchain/contracts/zk/CertichainAccessGate.sol, blockchain/contracts/zk/SemaphoreImports.sol
  • blockchain/contracts/AcademicRecords.sol and blockchain/contracts/abstract/, interfaces/, libraries/, modules/
  • src/contracts/v03/manifest.ts, src/services/contracts/contract-gateway.ts

Tests​

The contract suites live in blockchain/test/ and are described in Contract tests, Invariants and gas and Zero-knowledge tests.