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
| Contract | Folder | Role | Admin |
|---|---|---|---|
IssuerRegistry | blockchain/contracts/v03/ | Who may issue, their unique names, key retirement, the guardian role and the one pause flag of a set | Default admin with a delayed two-step transfer |
CredentialRegistry | blockchain/contracts/v03/ | Anchors credentials signed by an issuer (EIP-712), the single entry point of a set | None |
StatusRegistry | blockchain/contracts/v03/ | Status of each credential (active, suspended, revoked, superseded) and issuer-wide suspension | Default admin with a delayed two-step transfer |
HolderRegistry | blockchain/contracts/v03/ | Binds a student wallet to an issuer's enrolment commitment through signed invitations | None |
RecordTypeRegistry | blockchain/contracts/v03/ | Custom record types each issuer defines for itself | None |
CredentialTypes | blockchain/contracts/v03/libraries/ | Shared structs and the status enum | Not a contract instance |
CertichainTimelock | blockchain/contracts/v03/ | The OpenZeppelin timelock, run by the admin Safe, that delays admin actions | Itself |
VersionAnnouncer | blockchain/contracts/v03/ | Append-only notice board of contract sets | Default admin |
CertichainAccessGate | blockchain/contracts/zk/ | Semaphore groups per credential, anonymous proofs of access, receipts | None |
AcademicRecords and modules | blockchain/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:
CredentialRegistryholds 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()).StatusRegistrytakes the issuer registry in its constructor and is linked to its credential registry once, after deployment, throughbindCredentialRegistry. Only that credential registry may callinitialiseandsupersedeFor.- The pause flag lives in
IssuerRegistryonly. The other contracts askissuerRegistry.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.
CertichainTimelockis not referenced by any registry. It becomes the admin ofIssuerRegistryandStatusRegistrythrough their two-step admin transfer.
Design rules that hold across the set
| Rule | Where it is enforced |
|---|---|
| No plaintext personal data on chain: subjects and metadata appear only as salted commitments and hashes | Data 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 transaction | CredentialRegistry._issue |
| Nothing is deleted. Status moves forward; revoked and superseded are terminal | StatusRegistry transition checks; invariant suite |
| Pause stops creation only. Corrections (revoke, suspend, reinstate, supersede, unbind) keep working | blockchain/test/v03/PauseClassification.test.ts |
| No function loops over unbounded storage. Lists are rebuilt from events | blockchain/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 credential | Function tables on each page |
| Contracts are not upgradeable. A change ships as a new set beside the old one | Contract set upgrade |
Pages in this section
| Page | What it covers |
|---|---|
| Credential types | The shared structs, the status enum and the EIP-712 type strings |
| Issuer registry | Issuers, names, key retirement and lineage, guardian, pause |
| Credential registry | Issuance checks, credential ids, atomic re-issue |
| Status registry | The status machine, reasons, admin holds, issuer suspension, isValid |
| Holder registry | Invitations, binding epochs, rebinding and unbinding |
| Record type registry | Built-in, reserved and custom type ids |
| Access gate | Semaphore groups per credential, scopes, epochs, nullifiers and receipts |
| Timelock | The admin timelock, its roles, delays and the handover |
| Version announcer | The advisory notice board of sets |
| Legacy v0.2 | AcademicRecords, RoleManager, RecordStorage, StudentManagement |
| Events and errors | Every event and custom error in one reference |
| Gas and limits | Gas snapshot figures, size budgets and every hard limit |
| Deployment and scripts | Compiler 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 service | Contract calls |
|---|---|
src/services/domain/credential-service.ts | issue, issueReplacing, getCredential, contractVersion |
src/services/domain/status-service.ts | suspend, reinstate, revoke, supersede, supersedeToRegistry, statusOf, isValid, isIssuerSuspended |
src/services/domain/enrolment-service.ts | redeem, rebind, unbind, revokeInvitations, holderOf, enrolmentOf, epochOf |
src/services/domain/record-type-service.ts | registerType, setTypeActive, typeOf |
src/services/domain/platform-governance-service.ts | pause, paused, isGuardian, defaultAdmin, pendingDefaultAdmin, timelock reads |
src/services/domain/access-service.ts and access-requester-service.ts | admit, 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.solblockchain/contracts/v03/libraries/CredentialTypes.solandblockchain/contracts/v03/interfaces/blockchain/contracts/v03/CertichainTimelock.sol,blockchain/contracts/v03/VersionAnnouncer.solblockchain/contracts/zk/CertichainAccessGate.sol,blockchain/contracts/zk/SemaphoreImports.solblockchain/contracts/AcademicRecords.solandblockchain/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.