Contract versions and sets
Certichain has had two contract generations, and the current one is deployed as versioned, immutable sets. This page explains both ideas and how the app copes with several live deployments at once.
Two generations
| v0.2 (legacy, release v2.0.0) | v0.3 (current) | |
|---|---|---|
| Shape | One AcademicRecords contract with a linked RecordStorage library and a StudentManagement module | Five small registries: issuer, credential, status, holder, record type |
| Library | OpenZeppelin 4.9 AccessControl, Pausable | OpenZeppelin 5: AccessControlDefaultAdminRules, Pausable, EIP712, SignatureChecker |
| Roles | Super admin, admin, university, student | Admin (delayed two-step), guardian, issuers; holders bind through invitations |
| What a record holds | Student id, student name, student wallet, institution name, CID, metadata hash, record type, timestamps | Issuer, salted subject commitment, document SHA-256, salted metadata hash, record type, issue date, optional expiry, schema version |
| Issuer approval | The transaction sender's role | An EIP-712 signature over the credential, carried in an event and the package, checked for EOAs and ERC-1271 wallets |
| Status | Records can be deleted with the student | Active, suspended, revoked, superseded, with reasons, history and expiry; nothing is ever deleted |
| Personal data on chain | Yes (names, student ids, wallets, CIDs) | No |
| Documents | Plaintext on IPFS, the CID on chain | Encrypted in the browser; the CID only in the holder's package |
| Errors | Revert strings | Custom errors |
| App flag | NEXT_PUBLIC_CONTRACT_VERSION unset or anything but v03 | NEXT_PUBLIC_CONTRACT_VERSION=v03 |
| Sepolia | AcademicRecords at 0x475Bb8555d160c85f0175df117576193984C8083 | Sets 0.3.0 and 0.5.0, below |
The app still serves both: readContractVersion() in src/lib/contract-version.ts picks the system, and pages with two behaviours keep a Legacy* component for v0.2 (for example src/app/dashboard/LegacyDashboardPage.tsx). New work targets v0.3 only. The legacy contract is described in Legacy v0.2 and its roles in Legacy v0.2 roles.
Versioned immutable sets
The v0.3 contracts are not behind upgradeable proxies. A change to contract code is shipped as a new complete set deployed next to the existing ones (decision ADR-0088):
- A set is one deployment of
IssuerRegistry,CredentialRegistry,StatusRegistry,HolderRegistryandRecordTypeRegistry. Sets may share anIssuerRegistrywhen the code allows it, so registering or pausing applies to all of them. - Old sets stay live for ever. Their credentials keep verifying in their own registries and are never copied.
- One set is active: new credentials are issued there. Every other surface (verify page, API, SDK, status lists, monitor, dashboard, inbox) reads all listed sets.
- A holder moves to a newer set only by a new issuance. The issuer re-issues and the old credential is marked superseded in its own set, with a hint naming the new registry.
Why immutable sets instead of proxies: a verifier pins registry addresses and can be sure the code behind them never changes; there is no upgrade key to steal; and an issued credential id (which commits to the registry address) stays valid in the registry that issued it.
The deployment manifest
Contract addresses are never configured by hand. Each chain has one generated manifest, src/contracts/v03/deployment.<chainId>.json, bundled into the app and read by the scripts and the server (src/contracts/v03/manifest.ts). It lists every set with its label, deployment time, deployment block (where event scans start), ABI version and five addresses; names the active set; and, once recorded, the timelock. The access gate has its own manifest in src/contracts/zk/deployment.<chainId>.json, bound to one credential registry. Field-by-field reference: Deployment manifest.
Sepolia today (as of 2026-10-24)
| Set | Deployed | Status | Notes |
|---|---|---|---|
| 0.3.0 | 2026-10-13, block 11904844 | Active | Admin is the Safe on both registries that have one. Built from the source before the revisions below. |
| 0.5.0 | 2026-10-17, block 11932876 | Listed, not active | Has its own IssuerRegistry with key lineage. Its registries are administered by the timelock. Issuers of 0.3.0 are not yet registered in it, so it cannot be activated yet. |
| Contract | Set 0.3.0 | Set 0.5.0 |
|---|---|---|
| IssuerRegistry | 0xf401fDe78884bd2a1809BC479Bcb67e0217578fA | 0x5c1cc061675BF1b83e16102F074c3f2c444bf94a |
| CredentialRegistry | 0x53Fb56822E507bAf5c0221D993108495CE1931ea | 0x1969950754144Af2ecEAa3A78F96e58Bf19D4297 |
| StatusRegistry | 0x59A4298f3aACb6E5EA001C5a94DA7DE731bcB54E | 0x5df854C1380c8A48DE158ee02480309b837554A2 |
| HolderRegistry | 0x58659C14c955B31B442f23822ae82d37961423d3 | 0x882817b7f50C3be9c878b1E1146589570eB5DF96 |
| RecordTypeRegistry | 0x1ce73AF9DF31935d7F7231217a64660030670b3B | 0xa93Cf8f90CbFBd326A929D808432c8CB7216bA93 |
Other Sepolia contracts: the timelock 0x33a22Cc4D86A62Ae141349B768C054b1DD794f33, and the access gate 0xFd75Ee64dA6127BA4D61799920291E5016518af6 bound to the 0.3.0 CredentialRegistry. The Safe's handover of the 0.3.0 registries to the timelock could be scheduled from 2026-10-20; the repository has no record that it was completed. See Deployments and Governance.
Revisions in the current source
The contract source in blockchain/contracts/v03/ reports contractVersion() "0.5.0" (the VersionAnnouncer reports "0.4.0"). It carries revisions that the deployed 0.3.0 set does not have. The app asks each registry for its version at run time, so one build works with old and new sets (src/lib/contract-revisions.ts: any plain x.y.z label from 0.4.0 upward counts as revised).
| Revision | What it adds | Why |
|---|---|---|
| R1 | issueReplacing: issue a new credential and supersede the old one in one transaction | A re-issue in two transactions could leave two live credentials if the second failed |
| R2 | Signature lookup by event topic, without depending on a stored block number | On some chains block.number is not the block the RPC reports |
| R3 | Supersede events carry the replacement's registry address | Lets a verifier follow a replacement into another set |
| R4 | supportsInterface (ERC-165) and contractVersion() on every registry | A verifier can confirm what it talks to |
| R5 | A rule: any change to a signed type changes the EIP-712 domain version | Keeps ids of different type shapes apart |
| R6 | Reserved for batch anchoring | Designed, built only if the chosen production chain makes it worthwhile |
| R7 | Issuer key retirement with a successor (key lineage, currentIssuerOf) | An institution can move to a new key and keep older credentials in good standing |
| R8 | The guardian may unpause | Recovery is not held back by the admin delay |
A set from contract version 0.5.0 on needs an IssuerRegistry with key lineage, so it cannot share the 0.3.0 issuer registry. Two sets of 0.5.0 or later can share one. See Key retirement and Contract set upgrade.
How the app routes between sets
A credential id does not say which set issued it. The app resolves that in one place:
- By id (verify page, API):
CredentialLocator(src/services/domain/credential-locator.ts) asks the active set first, then the older ones, and returns services bound to the set that holds the credential. Every later check (status, signature, record type, lineage, management actions) runs against that set. - By file (package, VC, badge, disclosure, presentation): the file names the registry it was signed for (the EIP-712
verifyingContract), and the app picks the listed set with that registry. A file naming an unlisted registry is refused, not checked elsewhere. - Lists (issuer dashboard, holder inbox, status lists, monitor): scans run over every listed set and are merged, each row tagged with its set.
- New issuance always goes to the active set. A re-issue of an older-set credential supersedes it in its own set.
Details: Contract set routing.
Source files
src/lib/contract-version.ts,src/lib/contract-revisions.tssrc/contracts/v03/manifest.ts,src/contracts/v03/deployment.11155111.json,src/contracts/zk/deployment.11155111.jsonsrc/services/domain/credential-locator.ts,src/services/domain/set-fan-out.ts,src/services/domain/routed-credentials.tsblockchain/contracts/v03/,blockchain/contracts/AcademicRecords.solblockchain/deployments/sepolia.jsondocs/process/runbooks/contract-sets.md,docs/specs/P4-S04-upgrade-strategy.md