Contract tests
The contracts are tested with Hardhat, Mocha, Chai and ethers v6 in blockchain/test/. The suite runs every contract on Hardhat's in-process network, with real signatures, real Semaphore proofs and a real Safe 1.5.0 where those matter. A full local run on 2026-10-24 reported 780 passing, none failing, in about one minute.
This page covers the unit and integration tests of the contracts and their scripts. The random-sequence invariant suite and the gas snapshot have their own page, Invariants and gas, and the proof system checks are on Zero-knowledge tests.
Running the tests
From blockchain/ (after npm ci in the repository root and in blockchain/, because some tests import shared TypeScript from src/lib/):
| Command | What it runs |
|---|---|
npm test | The whole suite (every file under test/) |
npx hardhat test test/v03/StatusRegistry.test.ts | One file |
npm run coverage | hardhat coverage, then scripts/check-coverage.ts enforces the thresholds |
npm run gas | The whole suite with the gas reporter, written to gas-report.txt |
npm run sizes | Compile and check contract size budgets |
The ZK tests that build real proofs need the pinned circuit files in blockchain/.zk-artifacts; npm run zk:artifacts downloads and hash-checks them.
Layout
| Folder | Purpose |
|---|---|
blockchain/test/ (top level) | v0.2 AcademicRecords and its parts; fixtures.ts deploys RecordStorage and links it |
blockchain/test/fixtures/ | Shared vectors used by both contract and app tests: credential-vector.json, invitation-vector.json, commitment-vectors.json |
blockchain/test/v03/ | The v0.3 registries, revisions, multi-set behaviour, governance, timelock and the deployment scripts |
blockchain/test/v03/helpers/ | deploy-system.ts (a full set with a 3 day admin delay), sign-credential.ts, sign-invitation.ts, issue-package.ts, safe.ts (deploys a real Safe), timelock.ts, source-scan.ts |
blockchain/test/v03/behaviours/ | registries.behaviour.ts: interface-level behaviour suites (behavesLikeIssuerRegistry and others) run against the real contracts in Skeleton.test.ts |
blockchain/test/zk/ | The access gate, Semaphore properties, the browser and Node provers, and the proof system supply chain checks |
blockchain/test/gas/ | Gas snapshot, operation replay, network cost model |
blockchain/test/invariants/ | Reference model, action generators, executor and invariant checks |
The static count of it( blocks is lower than the run-time count, because some tests are generated in loops: the status transition matrix alone generates 72 cases, and the pin checks generate one case per refused version range.
What each group covers
v0.3 registries
| File | Covers |
|---|---|
CredentialIssuance.test.ts | Anchor and events, ids equal to the client's, wrong signer, field changed after signing, malformed and high-s signatures, removed and unregistered issuers, nonce reuse, ids per deployment, zero fields, future issuedAt, inverted validity, ERC-1271 issuers, the shared digest vector |
CredentialRecordTypes.test.ts | Issuance with built-in, reserved, unregistered, foreign and inactive type ids |
StatusRegistry.test.ts | Every (state, action, caller) combination, initialise and binding, reason codes, supersede rules, issuer suspension, expiry, removed issuers, no delete function |
ExpiryBoundary.test.ts | Valid at validUntil, invalid one second later, on a fixed timestamp |
HolderRegistry.test.ts | Redeem rules, namespaces, rebind and unbind, no admin path, invitation epochs, the invitation vector |
RecordTypeRegistry.test.ts | Ranges, per-issuer numbering, names, bounds, switching, typeOf, isTypeAllowed |
IssuerRegistry.test.ts | Add, rename, remove, unique names, the 128 byte bound counted in bytes |
KeyLineage.test.ts | Key retirement, eight hops, what a retired key can no longer do, good standing following the successor, guardian unpause |
Skeleton.test.ts | The shared behaviour suites, delayed two-step admin, single binding |
BoundedWork.test.ts | No unbounded loops, pushes or storage arrays; no function returns a list; gas flat at 1, 100 and 1,000 stored items |
NoPlaintext.test.ts | No student name, id, wallet or CID in any log or storage slot after issuance; commitment vectors reproduced with plain ethers |
PauseClassification.test.ts | Every state-changing function classified as stopped or working while paused |
ContractSize.test.ts | The size helper and the 20,000 byte budget |
Revisions.test.ts | R1 atomic replacement, R2 no stored block, R3 successor registry hint, R4 interface and version getters, R5 signing domain rule |
TwoSets.test.ts | Two sets side by side: different ids, revoking in the old set, forward links, isolation of status registries, one pause for a shared issuer registry |
VersionAnnouncer.test.ts | Announcements, admin only, one per label, coherence check |
Governance and timelock
| File | Covers |
|---|---|
Governance.test.ts | Two-step admin transfer on both registries, guardian role, pause coverage, status registry construction |
SafeGovernance.test.ts | A real Safe 1.5.0 as admin: becomes admin, cannot accept early, adds an issuer and restarts only with two signatures, lifts a suspension, rotates a signer |
Handover.test.ts | The Safe handover plan checks and guardian membership read from logs |
Timelock.test.ts | Timelock roles, admin actions only after the delay, no double execution, atomic batches, each admin function waiting |
TimelockDeploy.test.ts, TimelockHandover.test.ts, TimelockProposal.test.ts, TimelockTargets.test.ts | Deployment plan checks, the handover step planner, Safe batch files and operation ids, target resolution |
Admission.test.ts, GovernanceLog.test.ts | Sign-off hashes as timelock salts, the admission report, the governance log format |
Deployment and verification scripts
| File | Covers |
|---|---|
Deploy.test.ts, RecordDeployment.test.ts | The v0.3 deploy (order, binding, guardian, shared issuer registry, labels, delays), adding and activating sets, refusing to overwrite a public manifest |
Manifest.test.ts, ManifestFile.test.ts | The manifest format and file reading |
SetReadiness.test.ts, IssuerList.test.ts | The readiness check before activating a set; parsing ISSUERS |
VerifyTargets.test.ts | Etherscan verification targets and constructor arguments |
VerifyCredential.test.ts, VerifyVc.test.ts | The offline verifier script for packages, verifiable credentials and badges |
Legacy v0.2
| File | Covers |
|---|---|
AcademicRecords.ts, Coverage.test.ts | Fixture, roles, records, access control, remaining branches |
StudentRegistration.test.ts, RecordPermission.test.ts | Admin-only student registration and migration; who may read a record |
Sharing.test.ts, ShareLimit.test.ts | Sharing rules and the 32 sharee cap |
Registry.test.ts, UniversityName.test.ts, UniversityNameIntegrity.test.ts | Admin and university lists, names taken from the registry, unique names |
Pause.test.ts | Paused and exempt functions |
Deployment.ts, Smoke.test.ts | Deployment tooling and the Sepolia smoke script |
Conventions the suite follows
- Fixtures, not shared state. Each test deploys what it needs through
loadFixtureor a helper, so tests are independent and order does not matter. - Errors by name. Reverts are asserted with
revertedWithCustomError(contract, "Name")for v0.3 and with the revert string for v0.2. - Real cryptography. Credentials and invitations are signed with ethers' EIP-712 signer; contract wallets use
MockERC1271Wallet; proofs come from the pinned Semaphore packages and circuit files. - Shared vectors. Where TypeScript and Solidity must agree (digests, commitments, the access scope), a JSON vector under
fixtures/is asserted on both sides. - Regression first for fixes. A security fix lands with a test that fails before and passes after; several test titles carry the step they come from (for example "(P1-S07 review)").
Coverage
npm run coverage runs solidity-coverage and then blockchain/scripts/check-coverage.ts:
| Measure | Threshold |
|---|---|
| Lines | 95 percent |
| Branches | 90 percent |
Files under interfaces/ are excluded. The gas snapshot test skips itself under coverage. CI uploads the coverage/ folder as the contract-coverage artifact.
Static analysis
The CI Slither job runs crytic/slither-action@v0.4.0 on blockchain/ after the Contracts job, with blockchain/slither.config.json:
{
"filter_paths": "node_modules|test",
"exclude_informational": true,
"exclude_low": false
}
It fails on any finding of high severity.
The CI job
The job is contracts in .github/workflows/ci.yml (Node 22). The full suite runs in the gas step, and again under coverage. As of 2026-10-24, GitHub CI did not run for the latest push because of a billing hold on the account, so the most recent checks were run locally. The wider pipeline is described in Continuous integration.
Source files
blockchain/test/blockchain/hardhat.config.ts,blockchain/package.jsonblockchain/scripts/check-coverage.ts,blockchain/slither.config.json.github/workflows/ci.yml