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.
229 lines
32 KiB
Markdown
229 lines
32 KiB
Markdown
# 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`
|