ASP Compliance
Association Set Provider architecture — how UPP enforces compliance without revealing user identity.
ASP Compliance
What is an ASP?
An Association Set Provider maintains a Merkle tree of approved addresses. When withdrawing from the pool, users prove their funds' origin is in an ASP's allowlist — without revealing which origin address they are.
How It Works
- Monitor deposits: ASP watches
Shieldevents, checks if the depositor meets criteria - Build Merkle tree: Approved addresses are organized in a Poseidon-hashed LeanIMT
- Publish root: Updated Merkle root is posted on-chain (per-block or on-schedule)
- User withdrawal: The ZK circuit proves that the note's origin is in the ASP's tree
// ASP backend (using @permissionless-technologies/upc-sdk)
pool.on('Shield', async (commitment, origin) => {
if (await meetsCriteria(origin)) {
await asp.addMember(origin)
await asp.publishRoot({ walletClient })
}
})Origin Semantics
Origin is the address that originally deposited funds into the pool.
Alice shields → origin = Alice
Alice → Bob transfer → origin still = Alice
Bob merges Alice's note → origin = Bob (Bob takes compliance responsibility)Origin is:
- Preserved through all transfers
- Only changed by merge (merger becomes new origin)
- The address checked by ASPs at withdrawal
Withdrawal Paths
Ragequit
Original depositors can always withdraw their own funds, regardless of ASP status. No hostage situations.
If an address is later sanctioned and removed from all ASPs, they can still execute a ragequit — withdrawing only to their own address (origin == recipient). Notes they previously sent to others become restricted, requiring the recipient to merge (taking on origin) or the original depositor to ragequit.
ASP Check Timing
ASP checks happen at exit, not entry. This is intentional:
| Operation | ASP Check | Notes |
|---|---|---|
| Shield | None | Anyone can deposit |
| Transfer | Required | Origin must be in an ASP |
| Merge | None | Origin changes to merger |
| Withdraw (normal) | Required | Origin must be in ASP tree |
| Withdraw (ragequit) | None | Recipient must equal origin |
Multiple ASPs
Different ASPs serve different use cases:
| ASP Type | Criteria | Use Case |
|---|---|---|
| OFAC sanctions screen | Address not on OFAC SDN list | General compliance |
| Institutional KYC | Verified institutional entity | B2B transactions |
| Exchange KYC | Passed exchange KYC | Exchange-integrated users |
| Government sanctions | National sanctions list | Jurisdictional compliance |
Recipients can require specific ASPs:
// A protocol that only accepts withdrawals from its own KYC ASP
function receiveFromPool(bytes calldata proof) external {
require(verifyASPMembership(proof, EXCHANGE_ASP_ID));
}Privacy Properties
ASPs know:
- The list of all depositors (Shield events are public)
- Which addresses meet their criteria
ASPs do NOT know:
- Who is withdrawing (the ZK proof hides identity)
- Which specific origin is being used in any given withdrawal
- Transaction patterns inside the pool
ASPs cannot censor specific withdrawals — they can only maintain their membership list.
The ASP compliance layer is built on @permissionless-technologies/upc-sdk. See the UPC documentation for running your own ASP.