Skip to main content

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/):

CommandWhat it runs
npm testThe whole suite (every file under test/)
npx hardhat test test/v03/StatusRegistry.test.tsOne file
npm run coveragehardhat coverage, then scripts/check-coverage.ts enforces the thresholds
npm run gasThe whole suite with the gas reporter, written to gas-report.txt
npm run sizesCompile 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​

FolderPurpose
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​

FileCovers
CredentialIssuance.test.tsAnchor 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.tsIssuance with built-in, reserved, unregistered, foreign and inactive type ids
StatusRegistry.test.tsEvery (state, action, caller) combination, initialise and binding, reason codes, supersede rules, issuer suspension, expiry, removed issuers, no delete function
ExpiryBoundary.test.tsValid at validUntil, invalid one second later, on a fixed timestamp
HolderRegistry.test.tsRedeem rules, namespaces, rebind and unbind, no admin path, invitation epochs, the invitation vector
RecordTypeRegistry.test.tsRanges, per-issuer numbering, names, bounds, switching, typeOf, isTypeAllowed
IssuerRegistry.test.tsAdd, rename, remove, unique names, the 128 byte bound counted in bytes
KeyLineage.test.tsKey retirement, eight hops, what a retired key can no longer do, good standing following the successor, guardian unpause
Skeleton.test.tsThe shared behaviour suites, delayed two-step admin, single binding
BoundedWork.test.tsNo unbounded loops, pushes or storage arrays; no function returns a list; gas flat at 1, 100 and 1,000 stored items
NoPlaintext.test.tsNo student name, id, wallet or CID in any log or storage slot after issuance; commitment vectors reproduced with plain ethers
PauseClassification.test.tsEvery state-changing function classified as stopped or working while paused
ContractSize.test.tsThe size helper and the 20,000 byte budget
Revisions.test.tsR1 atomic replacement, R2 no stored block, R3 successor registry hint, R4 interface and version getters, R5 signing domain rule
TwoSets.test.tsTwo 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.tsAnnouncements, admin only, one per label, coherence check

Governance and timelock​

FileCovers
Governance.test.tsTwo-step admin transfer on both registries, guardian role, pause coverage, status registry construction
SafeGovernance.test.tsA 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.tsThe Safe handover plan checks and guardian membership read from logs
Timelock.test.tsTimelock 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.tsDeployment plan checks, the handover step planner, Safe batch files and operation ids, target resolution
Admission.test.ts, GovernanceLog.test.tsSign-off hashes as timelock salts, the admission report, the governance log format

Deployment and verification scripts​

FileCovers
Deploy.test.ts, RecordDeployment.test.tsThe 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.tsThe manifest format and file reading
SetReadiness.test.ts, IssuerList.test.tsThe readiness check before activating a set; parsing ISSUERS
VerifyTargets.test.tsEtherscan verification targets and constructor arguments
VerifyCredential.test.ts, VerifyVc.test.tsThe offline verifier script for packages, verifiable credentials and badges

Legacy v0.2​

FileCovers
AcademicRecords.ts, Coverage.test.tsFixture, roles, records, access control, remaining branches
StudentRegistration.test.ts, RecordPermission.test.tsAdmin-only student registration and migration; who may read a record
Sharing.test.ts, ShareLimit.test.tsSharing rules and the 32 sharee cap
Registry.test.ts, UniversityName.test.ts, UniversityNameIntegrity.test.tsAdmin and university lists, names taken from the registry, unique names
Pause.test.tsPaused and exempt functions
Deployment.ts, Smoke.test.tsDeployment tooling and the Sepolia smoke script

Conventions the suite follows​

  • Fixtures, not shared state. Each test deploys what it needs through loadFixture or 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:

MeasureThreshold
Lines95 percent
Branches90 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.json
  • blockchain/scripts/check-coverage.ts, blockchain/slither.config.json
  • .github/workflows/ci.yml