aere-research/research/specs/spec-identity-compliance.md
Aere Network 4a0b48588c Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:30 +03:00

32 KiB

AERE Network Subsystem Spec: Identity and Compliance Stack

Status: mixed (live contracts on mainnet plus in-development pieces, marked inline) Chain: AERE Network, chain ID 2800 (Hyperledger Besu QBFT, 0.5-second blocks) Source of truth for addresses: sdk-js/src/addresses.ts Source of truth for logic: contracts/contracts/compliance/*.sol, contracts/contracts/AereIdentity.sol, zk-circuits/

0. Honest preamble (read this first)

This stack is the identity and regulatory-compliance layer of AERE Network. Before any of the design below, three facts about the environment it runs in must be stated plainly, because they bound how much trust any reader should place in it:

  1. AERE is currently operated by the Foundation as a small validator set (seven QBFT validators, one operator, one client implementation, Besu). It has not had an external security audit. Several of the compliance contracts here have zero or near-zero live usage (for example the privacy pool holds 0 deposits at the time of writing). We treat these as honest facts, not as things to hide.
  2. Every piece of this stack lives at the application and account layer. None of it changes consensus. Validators sign classical QBFT messages. The identity and compliance contracts are ordinary EVM contracts that apps opt into.
  3. Many of the "trust-minimized" claims below reduce to "trust the AERE Foundation to publish honest roots, and rely on an optimistic challenge window to catch it if it does not." Where that is the case we say so, and we describe exactly what a challenger can and cannot do.

A second theme runs through the whole document and deserves to be named up front: the difference between an inclusion proof and an absence proof. Most of the cryptography here proves that something IS in a set (an address is on a sanctions list, a credential is in an issuer's tree, a note is in a Foundation-attested "clean" set). Proving that something is NOT in a set is a fundamentally harder primitive, and where we do not have it, we say so rather than implying we do.

1. Architecture

The stack has three tiers:

  • On-chain anchors (the Solidity contracts). These store roots, hashes, commitments, and boolean flags. They never store personally identifying information (PII). The one exception in spirit is AereIdentity, whose claims are plaintext booleans on-chain (no PII, but the fact that address X holds a "KYC_TIER_1" claim from attestor Y is public).
  • Off-chain attestation and ingestion. The Foundation and third-party issuers run off-chain jobs that build Merkle trees (OFAC list, issuer credential sets, association sets) and publish roots through Foundation-gated calls.
  • Zero-knowledge provers. SP1 zkVM (Succinct Labs) guest programs prove attribute statements (over-18, jurisdiction, clean-set membership) off-chain; the resulting Groth16 proofs are verified on-chain through the shared SP1VerifierGateway at 0x9ca479C8c52C0EbB4599319a36a5a017BCC70628.

The contracts, all owned by (or gated to) the Foundation account 0x0243A4f47D44b40b65D33f20329dE20D00c6f3C3 (a single-key Foundation-controlled account, not a deployed multisig) where they have an owner:

Contract Address Role
AereIdentity 0x658dD2CD1F798AAb19fEc8FF69A270B2d192CaD1 DID and revocable-claim registry
AereZKScreen (v3) 0x3A097A459FD26aC79573aCB5adB51430e473C2f1 ZK attribute-proof anchor
AereSanctionsRegistry 0xb7d235718D99560F6EA4Fc5eAea2F8a306A3Cacf OFAC SDN Merkle registry
ChainalysisOracleWrapper 0x1B7Be82C80f368f75Cb3807B1bc05E86A498f85c Forwarding sanctions oracle
AereForensicEventRegistry 0x4a7526A068e5DDE9788f6571E4A99095b14C6fff Forta-style detection-bot alerts
AereTravelRuleHashRegistry 0xcF0E2e010E6e4506672019b1e570874AEeBC4c84 FATF Travel Rule hash anchor
AereCompliancePoolV2 0xB144c923572E5Ac1B6B961C4ccfec36917173465 Compliant privacy pool
AereCompliancePoolSP1Verifier 0xE2D3fa91b680E835c971761ba75Fde0204AEF95E SP1 adapter for the pool
AereAttestationGateway 0x9bdacA8dfF39Fc688e8D3c4bbA13bCFC0580c325 Regulator-attestation schema registry

2. AereIdentity (live)

Address 0x658dD2CD1F798AAb19fEc8FF69A270B2d192CaD1, owner Foundation. Solidity ^0.8.19. This is the oldest and simplest contract in the stack, deployed 2026-05-07 with the core batch.

It is a lightweight on-chain DID registry, compatible in spirit with W3C DID and ERC-1056. Three primitives:

  • Profiles. setProfile(string uri) lets any address point at a content-addressed profile document (for example ipfs://...). Only the pointer and a timestamp live on-chain.
  • Attestors. The owner maintains an allowlist (isAttestor) of trusted issuers (KYC providers, employers, communities). addAttestor / removeAttestor are owner-only.
  • Claims. An attestor calls issueClaim(subject, claimType, expiresAt, evidence). claimType is a keccak256 tag, for example keccak256("KYC_TIER_1"), keccak256("AGE_OVER_18"), keccak256("EMAIL_VERIFIED"). evidence is an optional hash of an off-chain document. Claims are keyed subject -> claimType -> attestor, so a subject can hold the same claim type from several attestors independently. revokeClaim lets the issuing attestor (and only the issuer) revoke. hasValidClaim(subject, claimType, attestor) returns true if the claim exists, is not revoked, and is not expired.

Honest scope and limitations:

  • Claims are plaintext booleans on-chain. AereIdentity reveals that a given address holds a given claim type from a given attestor. It is a transparency-positive, privacy-negative design. It is appropriate for public credentials (DAO membership, "email verified") and inappropriate for anything a user would want to keep private (residency, exact KYC tier). The zero-knowledge path for sensitive attributes is AereZKScreen (Section 3), not this contract.
  • The evidence field is a bare hash with no schema. AereAttestationGateway (Section 10) is the newer, schema-versioned, inheritance-aware successor for structured attestations; AereIdentity remains the simple key-value DID layer.
  • Ownership is a single owner address (the Foundation), not a per-claim governance model. There is no on-chain dispute path for a bad claim beyond the issuer's own revokeClaim.

Roadmap: a thin adapter that lets a dApp read an AereZKScreen clearance and an AereIdentity claim behind one interface is planned but not deployed. Today they are queried separately.

3. AereZKScreen v3: zero-knowledge attribute proofs (live anchor; over-18 program pending Foundation registration)

Address 0x3A097A459FD26aC79573aCB5adB51430e473C2f1 (v3), owner Foundation, SP1 verifier fixed at deploy to the gateway 0x9ca479C8c52C0EbB4599319a36a5a017BCC70628 (SP1 Groth16, selector 0x4388a21c). Deploy tx 0xf527bdcf1ccdbf41db27237b1004699335078a66f6a04a630554bbd2e1b468f5, deployed 2026-07-07. This is a Privacy-Pools-style compliance anchor: it records that a user passed an off-chain screen without any of the source data touching the chain.

3.1 Model

The Foundation registers "programs." A program is one compliance statement, identified by its SP1 programVKey (the verification key of a specific guest circuit). Examples the design targets: an over-18 attribute proof, an EU-jurisdiction / MiCA-eligibility proof, an accredited-investor proof, an OFAC-screen proof. A user runs the corresponding SP1 program off-chain, produces a Groth16 proof, and calls submitProof(programVKey, publicValues, proof). On success the contract stamps clearedAt[user][programVKey]. dApps gate access by reading isCleared(user, programVKey, minTimestamp).

The public-values convention is exactly abi.encode(uint256 chainId, address user, uint256 attestedAt, bytes32 extraDataHash), a fixed 128 bytes. submitProof enforces the exact length (rejecting trailing-byte spoofs), checks chainId == block.chainid, checks user == msg.sender, and rejects a proof whose attestedAt is in the future or older than the current stored clearance (stale-proof downgrade protection).

3.2 The root-binding soundness fix (why v1 and v2 are dead)

This is the most important design decision in the contract, and it is worth being candid that the first two deployments got it wrong.

  • v1 0xE9da9c5F40c2CDfda368885832C59609286e04ee (dead): declared the SP1 verifier interface as returns (bool). The real SP1 gateway returns nothing and reverts on an invalid proof, so decoding a bool from empty returndata reverted even for valid proofs. submitProof could never succeed. Fixed in v2 with a void interface plus try/catch.
  • v2 0x140572e018C0342336dbaAa1713281c15678aFa7 (dead, self-clear hole): the proof commits an extraDataHash interpreted as the allowlist Merkle root the screen was checked against. Because the prover supplies that root inside the zkVM, anyone could prove membership in a root of their own making (an "allowlist" containing only themselves), a total soundness break for the compliance claim.
  • v3 (live) closes it: each program binds a Foundation-set authorizedRoot, and submitProof requires extraDataHash == authorizedRoot (revert UnauthorizedRoot otherwise). The Foundation rotates the root via setAuthorizedRoot as the real allowlist changes, and can void an individual clearance via revoke(user, programVKey), which sets a per-user watermark so a clearance whose attestedAt is at or below the watermark reads as invalid.

isCleared also enforces an optional per-program expirySeconds. The contract source explicitly instructs consumers to pass a real minTimestamp (for example block.timestamp - freshnessWindow), never 0, and to treat clearance as necessary-not-sufficient, ANDing it with a live sanctions check for sanctions-sensitive flows.

3.3 The over-18 circuit (real; deep dive)

The guest program lives at zk-circuits/recursive-aggregation/over18-program/src/main.rs. It proves in zero knowledge that the prover holds an issuer-attested birth-year credential AND that the birth year implies adulthood, without revealing the birth year.

Mechanics:

  • A Foundation-approved KYC issuer publishes a Merkle root of keccak256(birthYear || user) leaves for the people it has verified.
  • The circuit reads public inputs (chainId, user, attestedAt, root) and private witness (birthYear, Merkle depth, and the sibling path).
  • It asserts birthYear <= MAX_ADULT_BIRTH_YEAR, where MAX_ADULT_BIRTH_YEAR = 2008 is a compile-time constant baked into the verification key. This makes the program a dated "adult as of 2026" attribute; it must be recompiled next year to advance the threshold. That is an honest design tradeoff (the constant is part of the vKey, so a stale program cannot silently drift).
  • It rebuilds the leaf keccak256(birthYear.to_be_bytes || user) and walks the witnessed path to the root, asserting membership. It commits exactly the AereZKScreen tuple, with extraDataHash carrying the issuer credential-tree root, so the same v3 contract anchors it.

Provenance: this circuit was proven end-to-end and its on-chain SP1 verification key is 0x005aa94ac711bc65d0755b3a94f3622a6e8c76e2cdd708c29869587f633343c5 (it also appears as a leaf in AERE's recursive proof-aggregation demo, alongside the general zkscreen program vKey 0x006a211015aee2b90b82d266bef3aa6944551b06c488d86e895489b1dde3e5b7).

Honest status: the over-18 circuit is real and the proof pipeline works, but registering it as a live program on the canonical v3 anchor (registerProgram(programVKey, expirySeconds, authorizedRoot, description), which is onlyOwner) with a Foundation-set authorizedRoot is pending a Foundation signature. Until that signature lands, submitProof for the over-18 program reverts UnknownProgram on the live contract. Mark this as in-development, not shipped.

3.4 EU-jurisdiction and other attribute programs (archetype; in development)

The v3 contract is program-agnostic: any SP1 circuit that commits the four-field tuple and binds an authorized root can be registered. The EU-jurisdiction / MiCA-eligibility proof, the accredited-investor proof, and the OFAC-screen proof are described in the contract as the intended program set, and they follow the identical pattern (issuer-attested credential root, in-circuit predicate over a private attribute, extraDataHash == authorizedRoot). Of these, only the over-18 circuit has a real guest program in the repository today. The others are archetypes on the same rail and should be presented as roadmap, not as live capabilities.

4. AereSanctionsRegistry: the OFAC Merkle registry (live), and the inclusion-vs-absence question

Address 0xb7d235718D99560F6EA4Fc5eAea2F8a306A3Cacf, Ownable, owner Foundation, Solidity 0.8.23. This is a read-only, public-good sanctions registry. There is no fee and no discretionary interception.

4.1 How it works

Each epoch (a 24h cron), an off-chain ingester reads the official OFAC Specially Designated Nationals (SDN) list from treasury.gov, extracts crypto addresses where present, builds a keccak256 Merkle tree, and the Foundation publishes the root. Leaf format: keccak256(abi.encode(address evmAddress, uint16 listId)), where listId distinguishes the source list. The contract reserves 1 = OFAC SDN, 2 = OFAC SSI, 3 = EU consolidated, 4 = UK HMT, 5 = UN consolidated; Phase 1 ships OFAC SDN only.

Lifecycle per epoch is optimistic, mirroring the NAV oracle pattern: Proposed -> (Challenged?) -> Attested. proposeSnapshot(epoch, root, sourceUrl, ipfsCid) is owner-only and records the canonical OFAC URL the root was built from plus an optional IPFS CID pinning the full list. Anyone may challenge(epoch, reason) within the 24h CHALLENGE_WINDOW; the Foundation may dismissChallenge. After the window elapses with no open challenges, anyone may attest(epoch), which freezes the root. History is append-only; attested epochs cannot be rewritten, and there is deliberately no "deSanction" path (removing an address would imply the Foundation is making a sanctions decision, which it is not; it only mirrors the canonical list).

Consumers call isSanctioned(epoch, evmAddress, listId, proof), which returns true only if the snapshot is attested AND the Merkle inclusion proof verifies. isSanctionedLatest(evmAddress, listId, proof) walks back from the latest epoch to the most recent attested snapshot, bounded at MAX_EPOCH_SCAN = 64 to prevent a griefable DoS (a Round-3 audit fix).

4.2 Inclusion, not absence (the honest core of this contract)

isSanctioned is an inclusion proof. It proves that an address IS on the list at a given epoch. It does NOT prove absence. A standard keccak256 Merkle root supports membership proofs only; proving non-membership would require a sorted or sparse Merkle tree (an SMT), which this is not. Consequently:

  • "This address is sanctioned" can be proven trustlessly on-chain (given the proof).
  • "This address is clean" cannot be proven by this contract. The best a consumer can do is: no valid inclusion proof was supplied. That is a much weaker, trust-the-published-list statement, and it depends on the caller (or an indexer) to actually attempt the lookup rather than skipping it.

For flows that need a positive "clean" assertion, the design intent is to compose this with a positive-allowlist primitive: the compliance pool's association-set inclusion proof (Section 8) or a ZKScreen OFAC-screen program (Section 3.4). Presenting the sanctions registry alone as "sanctions-clean gating" would overstate it, and this spec does not.

4.3 Trust and liveness

The published root is Foundation-attested through a Foundation-controlled key (Ledger-signed); the Foundation control point on chain 2800 is a single-key account today, not a deployed multisig (a Safe multisig is roadmap). The optimistic window lets anyone challenge a bad root, but a challenge only forces a dismissal decision; it does not itself prove the root wrong. The ultimate ground truth is the off-chain OFAC list, and the sourceUrl plus ipfsCid fields exist so third parties can reproduce and audit the tree. This is honest optimistic security, not trustless.

5. ChainalysisOracleWrapper (deployed; inert on 2800 until an upstream is set)

Address 0x1B7Be82C80f368f75Cb3807B1bc05E86A498f85c, Ownable, owner Foundation. Chainalysis publishes a free public isSanctioned(address) oracle on Ethereum mainnet and other major chains. This wrapper gives AERE dApps a single stable endpoint with the same interface, forwarding a STATICCALL to a configured upstream. On any upstream error (paused, migrated, not present) it returns false rather than reverting, so a consumer is never bricked.

Honest caveat: the Chainalysis oracle is not natively deployed on chain 2800. upstreamOracle is settable once by the Foundation via setUpstream and frozen by lockUpstream. Until an upstream is configured, isSanctioned returns false for every address, meaning the wrapper is effectively inert and must not be relied on as the sanctions source. The real on-chain sanctions signal today is the Merkle registry in Section 4. The wrapper is scaffolding for a future Chainalysis (or equivalent) presence on 2800, and it should be labeled as such.

6. AereForensicEventRegistry (live)

Address 0x4a7526A068e5DDE9788f6571E4A99095b14C6fff, Ownable, owner Foundation, Solidity 0.8.23. An open, Forta-style registry where detection bots post structured alerts about suspicious on-chain activity. Bots self-register (registerBot), then emitAlert(botId, subject, alertType, severity, detailUri). The alert taxonomy runs 0 (catch-all) through 9 (rug-pull pattern), including 1 (sanctioned-flow), 2 (sandwich/MEV), 5 (bridge exploit), 8 (honeypot). Severity runs 1 (info) to 5 (critical). Consumers read recentConfirmedAlertCount(subject, minSeverity, windowSeconds), bounded at MAX_RECENT_ITERATIONS = 256 (a Round-3 DoS fix, so an attacker spamming millions of low-severity alerts cannot revert a consumer's in-transaction query).

Honest scope: registration is permissionless, but in Phase 1 only the Foundation confirms alerts (confirmAlert), and the read that consumers gate on counts CONFIRMED alerts only. So the trust anchor is again the Foundation. Stake requirements are 0 in Phase 1 (purely reputational); an AERE bond plus staked-community confirmation is described as a Phase 2 item and is not built. This registry is an audit and signaling surface with no custody and no transaction interception.

7. AereTravelRuleHashRegistry (live)

Address 0xcF0E2e010E6e4506672019b1e570874AEeBC4c84, Solidity 0.8.23, no admin. This is the on-chain anchor for FATF Travel Rule message exchanges (Notabene / IVMS-101 compatible). The Travel Rule requires VASPs to exchange identity payloads for transfers above a jurisdiction threshold (roughly USD 1,000 for the US, EUR 1,000 under EU MiCA). The payloads are confidential and exchanged off-chain over a message bus. This contract anchors only a commitment.

Flow: the originating VASP calls commit(payloadHash, beneficiaryVasp, threshold), storing a keccak256 hash of the IVMS-101 payload, the two VASP addresses, the threshold (USD cents), and a timestamp; the id is indexed under both VASPs for audit retrieval. The beneficiary VASP later calls acknowledge(id) to mark receipt and verification, or dispute(id, reason) if the on-chain hash does not match what was exchanged. No payload data, and no identity, is ever stored on-chain. The anchor proves the payload existed at a specific block, is append-only, and is auditable by regulators without revealing content.

Design notes and honesty: there is no admin, no Foundation interception, and no custody. The contract is genuinely trust-minimized in the narrow sense that it is just an append-only commitment log; the VASPs self-identify via msg.sender and there is no on-chain VASP allowlist, so the registry does not itself vouch that a committing address is a licensed VASP (that verification is off-chain, and can be layered via AereAttestationGateway). It is a minimal viable Travel Rule anchor, Phase 1.

8. AereCompliancePoolV2: the compliant privacy pool (live; 0 deposits)

Address 0xB144c923572E5Ac1B6B961C4ccfec36917173465, Solidity 0.8.23, ReentrancyGuard. Deploy tx 0xca77b3a125ac2c9ae3687031e40c7e3e216300158c5785b0583fa231dce5fb85, block 8910143. It denominates in WAERE (0x7e84d7d66d5da4cfE46Da67CDEeB05B323e1f5e8) with a fixed DENOMINATION of 1 WAERE, FOUNDATION set to the Foundation account (single-key, not a deployed multisig), and VERIFIER set to the reused SP1 adapter 0xE2D3fa91b680E835c971761ba75Fde0204AEF95E (program vKey 0x00aa183a...). POOL_ID is 0x100bc0276cfe121913b849d9804a1575cb9894d7db22561eafe9a8ab50f747c8. It is a fresh pool: depositCount is 0. Present it honestly as a working, tested primitive with no real usage yet.

8.1 The Privacy Pools + Travel Rule construction

This is Ameen Soleimani / Vitalik-style Privacy Pools, hybridized with a Travel Rule hook. The core is an append-only keccak256 Merkle tree, TREE_DEPTH = 20 (capacity 2^20 = 1,048,576 notes), with a rolling 256-root history (ROOT_HISTORY_SIZE = 256) so a withdrawal can prove against any recent deposit root. ZERO_VALUE is keccak256("aere.compliance-pool.v1.empty-leaf"). These parameters are byte-for-byte identical to the v1 pool, which is what lets the same off-chain SP1 circuit and the same deployed verifier keep working.

Deposit path: deposit(commitment, travelRuleHash, complianceProvider, travelRuleUri). The complianceProvider must be on a Foundation-managed allowlist (isComplianceProvider, set via setComplianceProvider). The actual leaf inserted is keccak256(commitment || msg.sender) (an R5 leaf-binding fix so a deposit note is bound to its depositor). The travelRuleHash and travelRuleUri carry the off-chain Travel Rule commitment for the deposit, tying this pool to Section 7's regime.

Withdraw path: withdraw(proof, depositRoot, associationRoot, nullifierHash, recipient, feeRecipient, fee, refund). The contract checks the nullifier is unspent, the associationRoot is published (see 8.2), and the depositRoot is a known recent root, then calls the SP1 verifier over eight public inputs [poolId, depositRoot, associationRoot, nullifierHash, recipient, feeRecipient, refund, fee]. The SP1 guest proves: commitment = keccak256(secret || nullifier); actualLeaf = keccak256(commitment || depositor); depositRoot is the Merkle root over the deposit tree containing that leaf; associationRoot is the Merkle root over the "clean" subset containing that same leaf; nullifierHash = keccak256(nullifier); and recipient / feeRecipient / refund / fee are all bound so a front-runner cannot substitute them.

8.2 Inclusion in a Foundation-attested clean set (the honesty crux)

The compliance property is a positive inclusion proof, not an absence proof. To withdraw, a user proves their note is a member of the association root, which is a Foundation-published Merkle root over the subset of deposits judged "clean" (an Association Set Provider model). This is the honest, correct way to do compliant privacy: the user demonstrates they are in the good set, rather than the impossible-to-verify claim that they are not in some bad set. The tradeoff is explicit: the clean set is Foundation-curated, so a user whose legitimate deposit the Foundation omits cannot withdraw with privacy until included. That is a real centralization point and we state it.

Association roots go through an optimistic lifecycle with a bond, matching the sanctions registry philosophy:

  • proposeAssociationRoot(root) is Foundation-only.
  • challengeAssociationRoot(root, reason) is permissionless but requires a CHALLENGE_BOND of 100 tokens (100 WAERE), capped at MAX_DISMISSALS_PER_ROOT = 2.
  • The Foundation may dismissAssociationRootChallenge a provably-bad challenge within the 24h window (ASSOCIATION_ROOT_CHALLENGE_WINDOW = 86400), which burns the bond to the dead address and restarts the publish window.
  • publishAssociationRoot(root) finalizes after the window with no active challenge; withdraw then accepts it.

8.3 The F8 fix (why v1 is dead)

The v1 pool 0x79735c31F289F7A4d6Be3E02aaB70B544796D41d (0 deposits, left on-chain, harmless) had a real bug: challengeAssociationRoot pulled the 100-token bond but there was no path to return it to an honest challenger. If the Foundation dismissed, the bond was burned; if the Foundation simply never acted, the root stayed "challenged" forever with the bond locked. An honest challenger who was right always lost 100 tokens.

V2 fixes this. A challenge now resolves one of two ways. DISMISSED: the Foundation dismisses within the window, the bond is burned to 0x...dEaD exactly as v1 (preserving the anti-grief property, so griefing a valid root still costs the bond). UPHELD: if the Foundation fails to dismiss within the window, anyone can call reclaimUpheldChallenge, which refunds the bond to the challenger and permanently rejects the root (associationRootRejected), so a questioned set is never published. That is the safe default for a compliance pool. This was demonstrated live on a byte-identical, deployer-owned twin (0x736c249F458E9d0E0F97cD03427004a40a77337f, masked-immutable bytecode identity to the canonical): an upheld reclaim refunded 100 and rejected the root (reclaimTx 0x609d20af...), a dismissed challenge burned 100 to dead without refund (dismissTx 0x45d452e3...), and a dismissed-then-quiet valid root published (0x41117e01...).

Residual risk, stated plainly: there is a liveness assumption on the Foundation. It must dismiss baseless challenges within the window, otherwise the challenge stands and the root is rejected (bond refunded). This fails safe toward challengers and is strictly better than v1, but it is not a trustless mechanism, and a Foundation that goes offline can have valid roots rejected by a single unanswered challenge.

9. AereCompliancePoolSP1Verifier (live)

Address 0xE2D3fa91b680E835c971761ba75Fde0204AEF95E. A thin adapter bridging the pool's IPrivacyPoolVerifier.verify(bytes proof, uint256[] pubs) interface to the SP1 gateway. It ABI-encodes the eight public inputs into the exact struct the Rust guest commits (pool_id, deposit_root, association_root, nullifier_hash, recipient, fee_recipient, refund, fee), then does a low-level staticcall to SP1.verifyProof(PROGRAM_VKEY, publicValues, proof), returning true iff it does not revert. Both SP1 and PROGRAM_VKEY are constructor immutables. The off-chain prover is the SP1 program at zk-circuits/compliance-pool/program; keccak256 hashing and TREE_DEPTH = 20 in the circuit must match the contract, which they do.

10. AereAttestationGateway (live)

Address 0x9bdacA8dfF39Fc688e8D3c4bbA13bCFC0580c325, Solidity 0.8.23, FOUNDATION immutable. A unified, schema-versioned attestation registry for regulator judgments (MiCA CASP licence, EU AI Act conformity, SEC 1099-DA broker reporting, FCA / FINMA / MAS equivalents). It is the structured successor to AereIdentity claims.

Model: the Foundation publishes append-only Schema records (id, spec hash, semver, spec URI). Per schema, the Foundation authorizes specific attestor addresses (setAttestor), which may be the Foundation itself or, for some schemas, the customer's own NCA-registered auditor. An authorized attestor calls attest(schemaId, subject, parentId, payloadHash, validFrom, validUntil, payloadUri); only the hash and validity window live on-chain, no PII. Attestations support optional inheritance (a child references a parent, and validity chases the chain, so a revoked or expired ancestor invalidates the child), bounded by MAX_INHERITANCE_DEPTH = 8. Two audit fixes are worth noting: HIGH #23 requires a parent to attest the same subject (no fabricating transitive trust across unrelated subjects), and HIGH #24 only overwrites the schema-wide latestFor pointer when the prior is invalid or the new attestation is currently valid (no griefing a valid record with an already-expired one). The source estimates attest() at roughly 70k to 80k gas cold-path (a source-code estimate, not a measured figure). Revocation is issuer-only and permanent; isValidNow and subjectCurrentlyAttested are the read paths.

11. Trust model summary and inclusion-vs-absence table

The single most important honesty point for this stack: which statements are cryptographically trustless, which are optimistic (trust-plus-challenge-window), and which are plain Foundation attestations.

Primitive What it proves Proof type Trust anchor
Sanctions registry isSanctioned address IS on OFAC SDN at epoch Merkle inclusion optimistic (Foundation root + 24h challenge)
Sanctions registry "clean" (cannot prove absence) none available not provided; do not imply it
Compliance pool withdraw note IS in the clean association set ZK inclusion (SP1) optimistic; clean set is Foundation-curated
ZKScreen over-18 birth year in issuer tree AND <= 2008 ZK inclusion + predicate (SP1) issuer credential root (Foundation-approved issuer); program registration pending
ZKScreen EU-jurisdiction / others archetype, same rail ZK (roadmap) roadmap, not live
Travel Rule anchor a payload hash existed at a block hash commitment trustless log; VASP identity is off-chain
Attestation gateway a body attested a subject signed on-chain record the attestor (Foundation or auditor)
Forensic registry a bot alerted, Foundation confirmed on-chain record Foundation confirmation (Phase 1)
Chainalysis wrapper upstream says sanctioned forwarded call inert on 2800 until upstream set
AereIdentity claim attestor issued a claim plaintext on-chain flag the attestor; no privacy

Foundation-attested pieces, gathered in one place so nothing is buried: the sanctions Merkle root, the compliance-pool association (clean-set) root, the ZKScreen per-program authorizedRoot, the attestation-gateway schemas and attestor authorizations, the compliance-pool provider allowlist, and the forensic-registry alert confirmations are all Foundation-gated. The optimistic challenge windows (sanctions registry, compliance pool) reduce but do not eliminate this trust, and they carry a Foundation liveness assumption. We consider stating this an asset, not a weakness to hide: a compliance stack that pretends to be trustless when it is optimistic is worse than one that is honest about where the trust sits.

12. Roadmap and not-yet-live items

  • Over-18 program registration on the canonical ZKScreen v3 anchor (a Foundation registerProgram call with the authorized issuer root): pending one Foundation signature. The circuit and proof pipeline are real; the live registration is not yet on-chain.
  • EU-jurisdiction / MiCA-eligibility, accredited-investor, and OFAC-screen ZKScreen programs: archetypes on the same rail, guest circuits not yet in the repository.
  • Chainalysis wrapper upstream: no Chainalysis (or equivalent) oracle deployed on chain 2800; setUpstream / lockUpstream not yet called.
  • Sanctions registry additional lists (SSI, EU, UK HMT, UN): list ids reserved, Phase 1 ships OFAC SDN only.
  • Non-membership (absence) proofs: not provided by any current primitive; would require a sparse/sorted Merkle tree, which is a future design item, not present today.
  • Forensic registry staked confirmation and AereIdentity-to-ZKScreen adapter: designed, not built.
  • External security audit of the whole stack: not performed.

Appendix: canonical addresses (verbatim from sdk-js/src/addresses.ts)

  • Foundation account (single-key, Foundation-controlled, not a deployed multisig): 0x0243A4f47D44b40b65D33f20329dE20D00c6f3C3
  • WAERE: 0x7e84d7d66d5da4cfE46Da67CDEeB05B323e1f5e8
  • AereIdentity: 0x658dD2CD1F798AAb19fEc8FF69A270B2d192CaD1
  • AereZKScreen (v3, live): 0x3A097A459FD26aC79573aCB5adB51430e473C2f1
  • AereZKScreen v2 (dead, self-clear hole): 0x140572e018C0342336dbaAa1713281c15678aFa7
  • AereZKScreen v1 (dead): 0xE9da9c5F40c2CDfda368885832C59609286e04ee
  • AereSanctionsRegistry: 0xb7d235718D99560F6EA4Fc5eAea2F8a306A3Cacf
  • ChainalysisOracleWrapper: 0x1B7Be82C80f368f75Cb3807B1bc05E86A498f85c
  • AereForensicEventRegistry: 0x4a7526A068e5DDE9788f6571E4A99095b14C6fff
  • AereTravelRuleHashRegistry: 0xcF0E2e010E6e4506672019b1e570874AEeBC4c84
  • AereCompliancePoolV2 (canonical): 0xB144c923572E5Ac1B6B961C4ccfec36917173465
  • AereCompliancePool v1 (deprecated, 0 deposits): 0x79735c31F289F7A4d6Be3E02aaB70B544796D41d
  • AereCompliancePoolSP1Verifier: 0xE2D3fa91b680E835c971761ba75Fde0204AEF95E
  • AereAttestationGateway: 0x9bdacA8dfF39Fc688e8D3c4bbA13bCFC0580c325
  • SP1VerifierGateway: 0x9ca479C8c52C0EbB4599319a36a5a017BCC70628