Legacy v0.2 contracts
Before v0.3, Certichain was one contract, AcademicRecords, released as v2.0.0. It is still deployed on Sepolia and the app still supports it when NEXT_PUBLIC_CONTRACT_VERSION is not v03, but all new work targets the v0.3 registries. This page documents it for maintenance and for comparison.
v0.2 stores the student's name, the student id, the student wallet, the institution name and the IPFS CID of the document in contract storage. Everything in contract storage is public, whatever the getters check. The permission checks on its views control what the app shows, not what the chain reveals. v0.3 was designed to remove this; see Privacy design.
Structure
| Part | File | Role |
|---|---|---|
RoleManager | blockchain/contracts/abstract/RoleManager.sol | Abstract base: OpenZeppelin 4.9 AccessControl and Pausable, the three roles, university names |
AcademicRecords | blockchain/contracts/AcademicRecords.sol | Records, sharing, custom types, student registration, pause |
IAcademicRecords | blockchain/contracts/interfaces/IAcademicRecords.sol | The RecordType enum (32 values), the Record and CustomRecordType structs, events |
RecordStorage | blockchain/contracts/libraries/RecordStorage.sol | External library holding record, share and custom type logic; linked at deployment |
StudentManagement | blockchain/contracts/modules/StudentManagement.sol | Student id to wallet registry, owned by AcademicRecords |
All of it compiles with OpenZeppelin ^4.9.3 under the default Paris EVM profile and uses require strings, not custom errors.
Roles
| Role | How it is held | Powers |
|---|---|---|
| Super admin | The deployer, fixed forever in the immutable SUPER_ADMIN; also granted DEFAULT_ADMIN_ROLE, SUPER_ADMIN_ROLE and ADMIN_ROLE | Add and remove admins (cannot remove itself), everything an admin can do |
Admin (ADMIN_ROLE) | Added by the super admin | Add, remove and rename universities, register and migrate students, pause and unpause |
University (UNIVERSITY_ROLE) | Added by an admin with a unique name | Add records, delete a student's records, create and update custom types |
| Student | A wallet registered to a student id in StudentManagement | Share and unshare its own records, read its own records |
grantRole, revokeRole and renounceRole are blocked for the registry roles ("Use registry functions"); roles change only through addAdmin, removeAdmin, addUniversity and removeUniversity, which keep the lists returned by getAllUniversities and getAllAdmins consistent. There is no transfer of the super admin.
Records
A Record holds id, studentId, studentName, studentAddress, universityName, ipfsHash, metadataHash, recordType, timestamp, isVerified (always set to true on creation) and issuer.
| Function | Who | Notes |
|---|---|---|
addRecord(studentId, studentName, studentAddress, ipfsHash, metadataHash, recordType) | University, not paused | Six arguments. Binds the student id to the wallet on first use (_bindStudent); refuses "Student ID mismatch" or "Student wallet mismatch" when either is already bound differently. The university name is taken from the registry, not from the caller. Emits RecordAdded |
deleteStudent(studentId) | University, not paused | Removes that university's records for the student and revokes their shares. Emits StudentDeleted |
getRecord(recordId) | Anyone (view) | Returns the full record |
getRecordWithPermission(recordId) | Issuer, a wallet it is shared with, an admin, the super admin, or the record's own student (view) | Otherwise "No permission to view this record" |
getStudentRecords(studentId), getStudentRecordsByAddress(address), getUniversityRecords() | Views | getStudentRecordsByAddress loops over every record of the wallet and of its student id to remove duplicates, so its cost grows with stored data |
verifyRecord(recordId) | View | Returns isVerified |
recordAccess(recordId) | Anyone, not paused | Emits RecordAccessed |
getTotalRecords(), getTotalCustomTypes() | Views | Counters |
Sharing
| Function | Who | Notes |
|---|---|---|
shareRecord(recordId, sharedWith) | The record's student, not paused | At most 32 sharees per record ("Share limit reached"). Emits RecordShared |
unshareRecord(recordId, sharedWith) | The record's student, not paused | Emits RecordUnshared |
getSharedRecords(sharedWith), isRecordSharedWith(recordId, address) | Views |
The 32 sharee cap bounds the work deleteStudent does per record. The app rebuilds the "Shared With Me" list from RecordShared and RecordUnshared events (it needs NEXT_PUBLIC_START_BLOCK to know where to start scanning).
Custom record types and students
| Function | Who | Notes |
|---|---|---|
addCustomRecordType(name, description) | University, not paused | Returns a numeric id; emits CustomRecordTypeCreated |
updateCustomRecordType(typeId, isActive) | The university that created it, not paused | "Not the creator" otherwise; emits CustomRecordTypeUpdated |
getCustomRecordType(typeId), getUniversityCustomTypes() | Views | |
registerStudentFor(studentId, wallet) | Admin or super admin, not paused | Registers a student in StudentManagement. There is no student self-registration |
migrateStudentWallet(studentId, newWallet) | Admin or super admin, not paused | Moves a student id to a new wallet; existing share grants are kept |
StudentManagement accepts writes only from its owner, the AcademicRecords contract that deployed it ("Only AcademicRecords"). It refuses an empty id, a zero address, an id already registered and a wallet already used, and emits StudentRegistered and StudentAddressUpdated.
Pause
From the contract's own comment:
| Paused | Exempt |
|---|---|
addRecord, deleteStudent, shareRecord, unshareRecord, addCustomRecordType, updateCustomRecordType, recordAccess, registerStudentFor, migrateStudentWallet, addUniversity, setUniversityName | pause, unpause, addAdmin, removeAdmin, removeUniversity, grantRole, revokeRole, renounceRole; views are never paused |
v0.2 compared with v0.3
| Topic | v0.2 | v0.3 |
|---|---|---|
| Personal data | Name, id, wallet, CID in storage | Salted commitments and hashes only |
| Who proves authorship | The university role sent the transaction | The issuer's EIP-712 signature, carried in an event and checkable offline |
| Document binding | A CID and a metadata string | SHA-256 of the document bytes |
| Status | isVerified (always true); records can be deleted | Active, suspended, revoked, superseded; nothing deleted |
| Student link | Admin registers a wallet | Issuer-signed invitation redeemed by the wallet |
| Admin | A fixed super admin, plain roles | Delayed two-step admin, a Safe, then a timelock; a guardian for emergencies |
| Upgrades | None | Versioned sets side by side |
| Unbounded loops | getStudentRecordsByAddress, deleteStudent work grows with data | None |
Deployment on Sepolia
Deployed and verified for release v2.0.0 (ADR-0041):
| Contract | Address |
|---|---|
AcademicRecords | 0x475Bb8555d160c85f0175df117576193984C8083 |
StudentManagement | 0xA742a8C965468b1E59C84c5254Dcc507589accD1 |
RecordStorage (library) | 0x3aba3A4Ed6822bABc7ef801Ed7d922F9f5e69dcc |
npm run deploy:local and npm run deploy:sepolia in blockchain/ run blockchain/scripts/deploy.ts, which deploys RecordStorage, links it into AcademicRecords (blockchain/scripts/lib/deploy-system.ts), writes deployments/<network>.json and exports the ABIs to src/contracts/. See Deployment and scripts. The role-by-role view of the old app is in Legacy v0.2 roles.
Source files
blockchain/contracts/AcademicRecords.solblockchain/contracts/abstract/RoleManager.solblockchain/contracts/interfaces/IAcademicRecords.solblockchain/contracts/libraries/RecordStorage.solblockchain/contracts/modules/StudentManagement.solblockchain/scripts/deploy.ts,blockchain/test/fixtures.ts
Tests
All in blockchain/test/ (top level), using test/fixtures.ts, which deploys RecordStorage and links it:
| File | Covers |
|---|---|
AcademicRecords.ts | Fixture with the linked library, roles, adding and listing records, access control, pause |
StudentRegistration.test.ts | Admin-only registration and wallet migration, no self-claim |
RecordPermission.test.ts | Who may read a full record |
Sharing.test.ts, ShareLimit.test.ts | Sharing rules and the 32 sharee cap |
Registry.test.ts | Admin and university lists, blocked role functions |
UniversityName.test.ts, UniversityNameIntegrity.test.ts | Names from the registry, unique names |
Pause.test.ts | The paused and exempt lists |
Deployment.ts | Deployment tooling: linked deploy, deployments/<network>.json, safe network names, ABI export, missing environment on public networks |
Smoke.test.ts | The read and write checks of the Sepolia smoke script and its verify targets |
Coverage.test.ts | Remaining branches: admin management, non-super admins, university list edits |