Skip to main content

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)
ShapeOne AcademicRecords contract with a linked RecordStorage library and a StudentManagement moduleFive small registries: issuer, credential, status, holder, record type
LibraryOpenZeppelin 4.9 AccessControl, PausableOpenZeppelin 5: AccessControlDefaultAdminRules, Pausable, EIP712, SignatureChecker
RolesSuper admin, admin, university, studentAdmin (delayed two-step), guardian, issuers; holders bind through invitations
What a record holdsStudent id, student name, student wallet, institution name, CID, metadata hash, record type, timestampsIssuer, salted subject commitment, document SHA-256, salted metadata hash, record type, issue date, optional expiry, schema version
Issuer approvalThe transaction sender's roleAn EIP-712 signature over the credential, carried in an event and the package, checked for EOAs and ERC-1271 wallets
StatusRecords can be deleted with the studentActive, suspended, revoked, superseded, with reasons, history and expiry; nothing is ever deleted
Personal data on chainYes (names, student ids, wallets, CIDs)No
DocumentsPlaintext on IPFS, the CID on chainEncrypted in the browser; the CID only in the holder's package
ErrorsRevert stringsCustom errors
App flagNEXT_PUBLIC_CONTRACT_VERSION unset or anything but v03NEXT_PUBLIC_CONTRACT_VERSION=v03
SepoliaAcademicRecords at 0x475Bb8555d160c85f0175df117576193984C8083Sets 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, HolderRegistry and RecordTypeRegistry. Sets may share an IssuerRegistry when 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)​

SetDeployedStatusNotes
0.3.02026-10-13, block 11904844ActiveAdmin is the Safe on both registries that have one. Built from the source before the revisions below.
0.5.02026-10-17, block 11932876Listed, not activeHas 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.
ContractSet 0.3.0Set 0.5.0
IssuerRegistry0xf401fDe78884bd2a1809BC479Bcb67e0217578fA0x5c1cc061675BF1b83e16102F074c3f2c444bf94a
CredentialRegistry0x53Fb56822E507bAf5c0221D993108495CE1931ea0x1969950754144Af2ecEAa3A78F96e58Bf19D4297
StatusRegistry0x59A4298f3aACb6E5EA001C5a94DA7DE731bcB54E0x5df854C1380c8A48DE158ee02480309b837554A2
HolderRegistry0x58659C14c955B31B442f23822ae82d37961423d30x882817b7f50C3be9c878b1E1146589570eB5DF96
RecordTypeRegistry0x1ce73AF9DF31935d7F7231217a64660030670b3B0xa93Cf8f90CbFBd326A929D808432c8CB7216bA93

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).

RevisionWhat it addsWhy
R1issueReplacing: issue a new credential and supersede the old one in one transactionA re-issue in two transactions could leave two live credentials if the second failed
R2Signature lookup by event topic, without depending on a stored block numberOn some chains block.number is not the block the RPC reports
R3Supersede events carry the replacement's registry addressLets a verifier follow a replacement into another set
R4supportsInterface (ERC-165) and contractVersion() on every registryA verifier can confirm what it talks to
R5A rule: any change to a signed type changes the EIP-712 domain versionKeeps ids of different type shapes apart
R6Reserved for batch anchoringDesigned, built only if the chosen production chain makes it worthwhile
R7Issuer key retirement with a successor (key lineage, currentIssuerOf)An institution can move to a new key and keep older credentials in good standing
R8The guardian may unpauseRecovery 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.ts
  • src/contracts/v03/manifest.ts, src/contracts/v03/deployment.11155111.json, src/contracts/zk/deployment.11155111.json
  • src/services/domain/credential-locator.ts, src/services/domain/set-fan-out.ts, src/services/domain/routed-credentials.ts
  • blockchain/contracts/v03/, blockchain/contracts/AcademicRecords.sol
  • blockchain/deployments/sepolia.json
  • docs/process/runbooks/contract-sets.md, docs/specs/P4-S04-upgrade-strategy.md