Skip to main content

Events and errors

The v0.3 contracts return no lists, so events are the system's index. "My credentials", the issuer dashboard, the share inbox, the record type list, the access group members and the governance queue are all rebuilt by scanning logs. Errors are custom Solidity errors with arguments, which the app decodes into messages.

Which events feed which screens​

The holder events (HolderBound, HolderRebound, HolderUnbound) are not scanned: the inbox asks enrolmentOf for the connected wallet and then finds CredentialIssued logs by the indexed subject commitment. The server-side status lists read StatusChanged (src/lib/server/status-lists/chain-state.ts), and the monitor watches the governance events listed in src/lib/server/ops/checks/chain-events.ts.

Log scanning lives in src/services/indexing/ (behind a CredentialIndex interface with a browser cache) and src/lib/zk/group-events.ts. See Reading the chain.

v0.3 events​

CredentialRegistry​

EventIndexedOther fieldsEmitted by
CredentialIssued(bytes32 credentialId, address issuer, bytes32 subjectCommitment, uint32 recordType)credentialId, issuer, subjectCommitmentrecordTypeissue, issueReplacing
CredentialSigned(bytes32 credentialId, address issuer, uint256 nonce, bytes signature)credentialId, issuernonce, signatureissue, issueReplacing

Indexing subjectCommitment lets a holder find credentials for their enrolment commitment without anyone else learning who they are.

StatusRegistry​

EventIndexedOther fieldsEmitted by
StatusChanged(bytes32 credentialId, Status from, Status to, uint16 reason, address by, bytes32 noteHash)credentialId, byfrom, to, reason, noteHashinitialise (None to Active) and every transition
CredentialSuperseded(bytes32 credentialId, bytes32 replacementId, address successorRegistry)all threesupersede, supersedeFor, supersedeToRegistry
IssuerSuspensionChanged(address issuer, bool suspended, uint16 reason, address by)issuer, bysuspended, reasonsetIssuerSuspended
CredentialRegistryBound(address credentialRegistry)credentialRegistrybindCredentialRegistry

successorRegistry is this set's own credential registry for a replacement in the same set, and another set's registry otherwise (revision R3, from 0.4.0).

IssuerRegistry​

EventIndexedOther fieldsEmitted by
IssuerAdded(address issuer, string name, address addedBy)issuer, addedBynameaddIssuer
IssuerRemoved(address issuer, address removedBy)bothremoveIssuer
IssuerUpdated(address issuer, string name)issuernamesetIssuerName
IssuerRetired(address retired, address successor, address retiredBy)all threeretireIssuer (from 0.5.0)

HolderRegistry​

EventIndexedOther fieldsEmitted by
HolderBound(address issuer, bytes32 enrolmentCommitment, address wallet)all threeredeem
HolderRebound(address issuer, bytes32 enrolmentCommitment, address previousWallet, address newWallet)issuer, enrolmentCommitmentpreviousWallet, newWalletrebind
HolderUnbound(address issuer, bytes32 enrolmentCommitment, address wallet)all threeunbind
InvitationsRevoked(address issuer, bytes32 enrolmentCommitment, uint64 epoch)issuer, enrolmentCommitmentepoch (the new value)revokeInvitations

RecordTypeRegistry​

EventIndexedOther fieldsEmitted by
RecordTypeRegistered(address issuer, uint32 id, string name, string description)issuer, idname, descriptionregisterType
RecordTypeActiveChanged(address issuer, uint32 id, bool active)issuer, idactivesetTypeActive (only when the flag changes)

CertichainAccessGate​

EventIndexedOther fieldsEmitted by
MemberAdmitted(bytes32 credentialId, uint256 groupId, uint256 identityCommitment, address actor)credentialId, groupId, actoridentityCommitmentadmit
MemberRemoved(bytes32 credentialId, uint256 groupId, uint256 identityCommitment, address actor)credentialId, groupId, actoridentityCommitmentremoveMember
AccessProven(bytes32 credentialId, uint256 epoch, uint256 nullifier, address caller)credentialId, epoch, callernullifierproveAccess

The Semaphore contract emits its own events in the same transactions (for example GroupCreated, MemberAdded, MemberRemoved and ProofValidated, declared in ISemaphore and ISemaphoreGroups).

VersionAnnouncer​

EventIndexedOther fields
VersionAnnounced(bytes32 labelHash, string label, string abiVersion, address credentialRegistry, address issuerRegistry, address statusRegistry, address holderRegistry, address recordTypeRegistry)labelHashall others

Inherited OpenZeppelin events​

EventFromWhere
Paused(address account), Unpaused(address account)PausableIssuerRegistry
RoleGranted, RoleRevoked, RoleAdminChangedAccessControlIssuerRegistry, StatusRegistry, VersionAnnouncer
DefaultAdminTransferScheduled, DefaultAdminTransferCanceled, DefaultAdminDelayChangeScheduled, DefaultAdminDelayChangeCanceledAccessControlDefaultAdminRulesSame three
CallScheduled, CallExecuted, CallSalt, Cancelled, MinDelayChangeTimelockControllerCertichainTimelock
EIP712DomainChangedEIP712 (declared by EIP-5267, never emitted here)CredentialRegistry, HolderRegistry

v0.3 custom errors​

Shared names​

Several contracts declare an error with the same name. The selector is the same when the argument types are the same, so the app decodes them by name and contract.

ErrorDeclared inMeaning
SystemPaused()Credential, holder, record type registriesThe set's pause flag is on
NotIssuer(address)Issuer, credential, holder and record type registriesNot a registered, active or non-retired issuer, depending on the call
InvalidSignature()Credential and holder registriesSignature does not verify for the issuer (EOA or ERC-1271)

CredentialRegistry​

ErrorRaised when
SystemPaused()Issuing while paused
NotIssuer(address signer)Sender is not the credential's issuer, or is not an active issuer
RecordTypeNotAllowed(address issuer, uint32 recordType)Reserved id, foreign or unknown custom id, or switched-off type
InvalidCommitment()subjectCommitment is zero
InvalidMetadataHash()metadataHash is zero
ZeroDocumentHash()documentHash is zero
InvalidIssuedAt(uint64 issuedAt)issuedAt more than 5 minutes ahead of the block
InvalidValidity(uint64 issuedAt, uint64 validUntil)validUntil set and not after issuedAt
NonceAlreadyUsed(address issuer, uint256 nonce)Nonce spent before
InvalidSignature()Signature check failed
CredentialAlreadyIssued(bytes32 credentialId)Defensive: an anchor already exists for the id

StatusRegistry​

ErrorRaised when
UnknownCredential(bytes32 credentialId)No credential with that id in this set
NotAuthorised(address caller)Caller is not the key in charge, not the admin where the admin may act, or not the bound credential registry
InvalidTransition(Status from, Status to)The move is not allowed from the current status (also: supersede under an admin hold)
ReplacementInvalid(bytes32 credentialId)Replacement is the same id, zero, not Active, or has a different key in charge
InvalidReason(uint16 reason)Reason in the reserved range 8 to 999
AlreadyInitialised(bytes32 credentialId)initialise called twice
AlreadyBound()bindCredentialRegistry called twice
InvalidAddress()Zero address in the constructor or binding; a bad successor registry
RegistryMismatch(address credentialRegistry)The registry being bound does not name this status registry

IssuerRegistry​

ErrorRaised when
InvalidIssuer()addIssuer with the zero address
IssuerAlreadyRegistered(address account)addIssuer for a registered address
NotIssuer(address account)Remove, rename or retire an unregistered address
EmptyName(), NameTooLong(uint256 length), NameAlreadyInUse(string name)Name rules (1 to 128 bytes, unique)
KeyRetired(address account)Registering or retiring an already retired key
InvalidSuccessor(address successor)Successor is the same key or not an active issuer
LineageTooLong()The chain would exceed 8 hops

HolderRegistry​

ErrorRaised when
ZeroWallet()Invitation names no wallet
NotWallet(address caller)Sender is not the named wallet
InvitationExpired(uint64 expiry)Expiry has passed
ExpiryTooFar(uint64 expiry)Expiry more than 30 days ahead
NotIssuer(address signer)Issuer not active (redeem, rebind), or caller is a retired key (unbind, revoke invitations)
InvitationAlreadyRedeemed(bytes32 invitationId)Invitation used before
StaleInvitation(uint64 currentEpoch, uint64 invitationEpoch)The binding epoch moved since the invitation was signed
InvalidSignature()Signature check failed
CommitmentAlreadyBound(address issuer, bytes32 enrolmentCommitment)redeem for a bound enrolment
WalletAlreadyBound(address wallet)The wallet already holds an enrolment at this issuer
NotBound(address issuer, bytes32 enrolmentCommitment)rebind or unbind with no binding
SystemPaused()Redeem or rebind while paused

RecordTypeRegistry​

ErrorRaised when
SystemPaused()Registering while paused
NotIssuer(address account)Not an active issuer (register) or a retired key (switch)
EmptyTypeName(), TypeNameTooLong(uint256 length), TypeDescriptionTooLong(uint256 length)Name 1 to 64 bytes, description at most 256 bytes
TypeNameInUse(string name)The issuer used the name before
UnknownRecordType(address issuer, uint32 id)No such custom type for that issuer

CertichainAccessGate​

ErrorRaised when
AccessGate__ZeroAddress()Zero address in the constructor
AccessGate__Paused()Admit or prove while the set is paused
AccessGate__UnknownCredential(bytes32)No such credential in the served set
AccessGate__NotHolderOrIssuer(bytes32, address)Caller is neither the bound holder nor the (registered, for admission) issuer
AccessGate__CredentialNotValid(bytes32)isValid is false
AccessGate__ZeroCommitment()Identity commitment is zero
AccessGate__GroupFull(bytes32)64 commitments were already admitted over the credential's life
AccessGate__NoGroup(bytes32)Nobody was ever admitted
AccessGate__EpochOutOfWindow(uint256 epoch, uint256 current)Epoch not within one day of the current one
AccessGate__DepthNotServed(uint256 depth)Proof tree depth above 6
AccessGate__WrongScope(uint256 expected, uint256 actual)Proof made for another credential, epoch, gate or chain
AccessGate__WrongMessage(uint256 expected, uint256 actual)Proof bound to another submitting address
AccessGate__NullifierSpent(bytes32, uint256)This member already proved for this credential in this epoch
AccessGate__InvalidProof()Semaphore rejected the proof

VersionAnnouncer​

ErrorRaised when
InvalidLabel()Empty or longer than 32 bytes, or empty ABI version
LabelAlreadyAnnounced(string label)Label used before
SetMismatch()Zero address, or the five addresses do not belong together

Inherited OpenZeppelin errors​

ErrorWhen
AccessControlUnauthorizedAccount(address account, bytes32 neededRole)Missing role. pause, unpause and issuer suspension report GUARDIAN_ROLE as the needed role
EnforcedPause(), ExpectedPause()Pausing a paused set, unpausing a running one
AccessControlEnforcedDefaultAdminRules(), AccessControlEnforcedDefaultAdminDelay(uint48), AccessControlInvalidDefaultAdmin(address)Breaking the two-step admin rules (instant grant, accepting early, wrong nominee)
TimelockUnexpectedOperationState, TimelockInsufficientDelay, TimelockUnauthorizedCaller and othersTimelock misuse (executing early, scheduling with a short delay, scheduling without the proposer role)

How the app turns errors into messages​

Each registry has a small error module in src/lib/ that maps error names to user messages: credential-errors.ts, status-errors.ts, holder-errors.ts, record-type-errors.ts and zk/gate-errors.ts. The name comes from ethers' decoded revert, or, when a send fails at gas estimation and ethers leaves only raw data, from parsing that data against the contract's ABI (revertName in src/lib/credential-errors.ts). For example, RecordTypeNotAllowed becomes "This record type is not available to you. It may be switched off, or it may belong to another institution."

A wallet rejection is recognised separately (ACTION_REJECTED, or code 4001, in src/services/wallet/errors.ts, and "Transaction rejected in wallet" in src/lib/contract-errors.ts), so pressing reject is never reported as a contract failure.

Legacy v0.2​

v0.2 uses require strings. The main ones:

MessageContract and function
"Not super admin"RoleManager, super admin functions
"Cannot remove super admin", "Already an admin", "Not an admin"addAdmin, removeAdmin
"Invalid address", "Invalid name", "Name already in use"University and student registration
"Use registry functions"grantRole, revokeRole, renounceRole for registry roles
"University name not set", "Student ID mismatch", "Student wallet mismatch"addRecord
"Record does not exist", "No permission to view this record"Record reads
"Not a registered student", "Not your record", "Share limit reached"Sharing
"Not the creator", "Custom type does not exist"Custom types
"Only AcademicRecords", "Student ID already registered", "Student not registered"StudentManagement

Events: RecordAdded, RecordVerified, RecordAccessed, StudentDeleted, RecordShared, RecordUnshared, CustomRecordTypeCreated, CustomRecordTypeUpdated (IAcademicRecords); AdminAdded, AdminRemoved, UniversityAdded, UniversityRemoved, UniversityNameUpdated (RoleManager); StudentRegistered, StudentAddressUpdated (StudentManagement). The app rebuilds v0.2 sharing lists from RecordShared and RecordUnshared.

Source files​

  • blockchain/contracts/v03/interfaces/ICredentialRegistry.sol, IStatusRegistry.sol, IIssuerRegistry.sol, IHolderRegistry.sol, IRecordTypeRegistry.sol
  • blockchain/contracts/zk/interfaces/ICertichainAccessGate.sol
  • blockchain/contracts/v03/VersionAnnouncer.sol
  • blockchain/contracts/interfaces/IAcademicRecords.sol, blockchain/contracts/abstract/RoleManager.sol, blockchain/contracts/modules/StudentManagement.sol
  • src/services/indexing/, src/lib/zk/group-events.ts
  • src/lib/credential-errors.ts, status-errors.ts, holder-errors.ts, record-type-errors.ts, contract-errors.ts, src/lib/zk/gate-errors.ts, src/services/wallet/errors.ts

Tests​

The contract tests of each registry assert its errors with revertedWithCustomError (see Contract tests), and the invariant suite compares the outcome of every random action, success or error name, with the reference model (see Invariants and gas). The app's error modules have their own unit tests next to them in src/lib/.