Smart Contracts
Five Solidity contracts handle on-chain proof verification, VASP registration, sanctions oracle updates, compliance event recording, and oracle relay. All deployed on Sepolia testnet and verified on Etherscan.
All contracts use OpenZeppelin AccessControl for role-based permissions and Pausable for emergency stops.
Groth16Verifier
Auto-generated by SnarkJS. Verifies Groth16 proofs on-chain.
- Gas cost: ~220K per verification
- Immutable after deployment
- 16 public signals
ComplianceRegistry
Main entry point for on-chain compliance verification. Orchestrates checks across the Groth16Verifier, VASPRegistry, and SanctionsOracle.
function verifyAndRecord(
bytes32 transferId,
uint[2] calldata _pA,
uint[2][2] calldata _pB,
uint[2] calldata _pC,
uint[16] calldata _pubSignals,
bytes32 vaspDidHash
) external whenNotPaused returns (bool)Checks performed (in order):
- Transfer not already recorded (replay prevention)
- SanctionsOracle and VASPRegistry not paused
- SanctionsOracle not stale
- VASP is active in VASPRegistry
msg.sendermatches registered VASP wallet_pubSignals[11]matchesblock.chainid(chain binding)keccak256(address(this)) % BN128_Rmatches_pubSignals[12](contract binding)- Proof not expired (
block.timestamp <= _pubSignals[15]) - Sanctions root matches
sanctionsOracle.currentRoot() - Issuer root matches
vaspRegistry.issuerMerkleRoot() keccak256(transferId) % BN128_Rmatches_pubSignals[13](transfer binding)- Credential commitment not in revocation list
- Nullifier not already spent
Groth16Verifier.verifyProof()returns true
On success: marks nullifier as spent, records proof, emits ProofVerified event with blinded nullifier.
Additional functions:
revokeCredential(bytes32 commitment)— revoke a credential (requiresREVOKER_ROLE)isRevoked(bytes32 commitment)— check revocation statusisVerified(bytes32 transferId)— check if a transfer has a recorded proof
VASPRegistry
Stores registered VASPs with wallet addresses, jurisdictions, and discovery endpoints.
function registerVASP(
bytes32 didHash,
address wallet,
string calldata jurisdiction,
string calldata discoveryEndpoint
) external onlyRole(REGISTRAR_ROLE) whenNotPauseddidHash: keccak256 of the VASP’s DID stringwallet: the address authorized to submit proofsjurisdiction: ISO 3166-1 alpha-2 codediscoveryEndpoint: URL to the VASP’s/.well-known/clearproof.json
Other functions:
revokeVASP(bytes32 didHash)— deactivate a VASPreactivateVASP(bytes32 didHash, address newWallet)— re-enable with new walletupdateIssuerRoot(bytes32 newRoot)— update the trusted issuer Merkle rootupdateDiscoveryEndpoint(bytes32 didHash, string newEndpoint)— change discovery URLgetDiscoveryEndpoint(bytes32 didHash)— read a VASP’s discovery URLisActive(bytes32 didHash)— check if VASP is activeissuerMerkleRoot— public state variable, current issuer tree rootactiveVaspCount— number of non-revoked VASPs
SanctionsOracle
Stores the sanctions Merkle root with staleness checks, update cooldown, and history.
function updateRoot(
bytes32 newRoot,
uint32 _leafCount
) external onlyRole(ORACLE_ROLE) whenNotPausedConstraints on updates:
- 1-hour cooldown between updates (
UPDATE_COOLDOWN) - Leaf count floor: new count must be ≥ 50% of current (prevents accidental list clearing)
- Root cannot be zero
State:
currentRoot— current sanctions Merkle root (public)lastUpdated— timestamp of last updateleafCount— number of leaves in current treegracePeriod— configurable staleness threshold (default 24h, range 6h–168h)rootHistory— ring buffer of last 1000 root records
Functions:
isStale()— returns true ifblock.timestamp > lastUpdated + gracePeriodsetGracePeriod(uint256 newPeriod)— adjust staleness window (requiresDEFAULT_ADMIN_ROLE)historyLength()— number of stored root recordspause()/unpause()— emergency controls (requiresDEFAULT_ADMIN_ROLE)
SanctionsRootRelay
Adapter that receives sanctions root updates from an authorized relayer or cross-chain bridge and forwards them to the SanctionsOracle.
function receiveRoot(
bytes32 newRoot,
uint32 leafCount
) external onlyRole(RELAYER_ROLE)This contract holds ORACLE_ROLE on the SanctionsOracle. To upgrade the transport layer (e.g., from operator relayer to LayerZero), deploy a new relay contract, grant it ORACLE_ROLE, and revoke from the old one — no oracle redeployment needed.
Development
cd packages/contracts
npx hardhat compile
npx hardhat test # 26 testsDependencies: OpenZeppelin Contracts v5.6.1, Hardhat, ethers.js, TypeChain.