Skip to Content
DocumentationSmart Contracts

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

  1. Transfer not already recorded (replay prevention)
  2. SanctionsOracle and VASPRegistry not paused
  3. SanctionsOracle not stale
  4. VASP is active in VASPRegistry
  5. msg.sender matches registered VASP wallet
  6. _pubSignals[11] matches block.chainid (chain binding)
  7. keccak256(address(this)) % BN128_R matches _pubSignals[12] (contract binding)
  8. Proof not expired (block.timestamp <= _pubSignals[15])
  9. Sanctions root matches sanctionsOracle.currentRoot()
  10. Issuer root matches vaspRegistry.issuerMerkleRoot()
  11. keccak256(transferId) % BN128_R matches _pubSignals[13] (transfer binding)
  12. Credential commitment not in revocation list
  13. Nullifier not already spent
  14. 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 (requires REVOKER_ROLE)
  • isRevoked(bytes32 commitment) — check revocation status
  • isVerified(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) whenNotPaused
  • didHash: keccak256 of the VASP’s DID string
  • wallet: the address authorized to submit proofs
  • jurisdiction: ISO 3166-1 alpha-2 code
  • discoveryEndpoint: URL to the VASP’s /.well-known/clearproof.json

Other functions:

  • revokeVASP(bytes32 didHash) — deactivate a VASP
  • reactivateVASP(bytes32 didHash, address newWallet) — re-enable with new wallet
  • updateIssuerRoot(bytes32 newRoot) — update the trusted issuer Merkle root
  • updateDiscoveryEndpoint(bytes32 didHash, string newEndpoint) — change discovery URL
  • getDiscoveryEndpoint(bytes32 didHash) — read a VASP’s discovery URL
  • isActive(bytes32 didHash) — check if VASP is active
  • issuerMerkleRoot — public state variable, current issuer tree root
  • activeVaspCount — 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) whenNotPaused

Constraints 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 update
  • leafCount — number of leaves in current tree
  • gracePeriod — configurable staleness threshold (default 24h, range 6h–168h)
  • rootHistory — ring buffer of last 1000 root records

Functions:

  • isStale() — returns true if block.timestamp > lastUpdated + gracePeriod
  • setGracePeriod(uint256 newPeriod) — adjust staleness window (requires DEFAULT_ADMIN_ROLE)
  • historyLength() — number of stored root records
  • pause() / unpause() — emergency controls (requires DEFAULT_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 tests

Dependencies: OpenZeppelin Contracts v5.6.1, Hardhat, ethers.js, TypeChain.