Lifecycle
Where in a protocol's lifecycle should compliance checks happen? UPP's mapping as a worked example, and why the standard stays silent.
Lifecycle
Open by design: UPP is the worked example, not the answer
UPP gates exactly as the table on this page describes in our open-source preview. The UPC standard does not require any specific lifecycle mapping. That's a per-protocol choice, by design. This page documents UPP's choice with reasoning as one valid example. Other shapes (gate at entry, gate on exclusion, gate at every transfer) are equally valid and equally welcome to ship against the same IAttestationVerifier. If your protocol gates differently, we want to hear why.
Every privacy protocol has to decide where in its lifecycle a compliance check fires. Onboarding? Deposit? Transfer? Withdrawal? Audit? Different protocols pick different points, and the choice is consequential: it shapes anonymity sets, threat models, and which counterparty bears the screening cost.
The UPC standard does not make this choice for you. The wire format is silent on lifecycle. But silence is not advice, and protocol designers asking "where should I gate?" deserve a worked example with reasoning. This page shows UPP's mapping as one valid answer, with the principle that drove it and the alternatives that disagree.
UPP's lifecycle mapping. Five operations, three with ASP checks (Transfer, Withdraw, Swap), two without (Shield, Ragequit). The check fires only when value crosses the privacy boundary outward or to a counterparty, never on entry.
The principle: gate where value leaves
UPP's rule is simple: every spent note must include an ASP membership proof on its origin, with one exception, ragequit, where the circuit enforces recipient === origin so the user can always retrieve their own funds without ASP approval.
That single rule explains every row in the table below. Shield doesn't spend a note, so it doesn't gate. Transfer, merge, withdrawal, and swap all spend at least one note. Each spend proves the origin is currently in the chosen ASP root. The chosen ASP can differ per operation, which is the meaningful differentiator vs. exit-only models like Privacy Pools: a recipient can demand a specific ASP at transfer time, and a swap maker can require fillers to come from a specific ASP via requiredFillerAspId.
Other shapes are valid for different threat models. Privacy Pools v1 gates only at withdrawal (internal transfers don't re-check); PPOI gates effectively at entry, by attempting to generate a proof of innocence at shield time and freezing funds in compliant channels if the proof can't be made. We don't think those are wrong. UPP's choice puts ASP enforcement at every note spend so a recipient is never forced to accept a note whose origin has been kicked out of the relevant ASP, and so a sanctioned origin can never be merged into a clean identity.
UPP's lifecycle table (full)
The five-stage diagram simplifies for visual clarity. The complete UPP mapping includes Merge as well:
| Operation | ASP check | Why |
|---|---|---|
| Shield (entry) | None | No note spent, nothing to verify. |
| Transfer | Required (input origin) | Spent note's origin must be in the chosen ASP root; recipient can demand a specific ASP. Origin is preserved to outputs. |
| Merge | Required (both input origins) | Both inputs' origins must be ASP-approved before being absorbed into the merger's identity. Without this, a sanctioned origin could be laundered through the merge. |
| Withdraw (normal) | Required (origin) | Spent note's origin must be ASP-approved to exit the pool cleanly. |
| Withdraw (ragequit) | None | Recipient = origin enforced in circuit; user can always retrieve their own funds. |
| Swap order (maker) | Required (maker's origin) | Maker's spent note origin must be in the public aspId posted with the order. |
| Swap fill (filler) | Required (filler's origin) | Filler's spent note origin must be in maker's requiredFillerAspId. |
How other protocols differ
UPC's wire is silent on lifecycle, so other protocols can ship different mappings without needing a different verifier ABI:
- Privacy Pools (0xbow) gates only at withdrawal. Anyone can deposit; once inside the pool, internal transfers don't re-check; only the exit to public chain requires an ASP membership proof.
- RAILGUN PPOI gates effectively at entry, with a non-membership flavor. At shield time the system attempts to generate a proof of innocence (your funds aren't traceable to known-bad sources). Funds with a valid proof can move through compliant channels; funds without one are stuck. The proof is immutable, so a false positive has no remediation. (See our ASP vs PPOI deep-dive for the full mechanics.)
- An ERC-3643-permissioned token gates at every transfer of the token itself. Every state change touches the compliance module.
All four shapes are expressible against the same IAttestationVerifier. The verifier doesn't know or care which lifecycle stage called it.
What the standard does and doesn't define
| Question | UPC's answer |
|---|---|
| Which lifecycle stages can fire a check? | Any. The verifier ABI is callable from anywhere: entry, transfer, exit, audit, off-chain pre-flight. |
| Which stages must fire a check? | None. That's the protocol's choice. |
| What happens if eligibility changes mid-lifecycle? | Up to the protocol. The standard supports per-attestation expiry (proposed) and per-root TTL (today); how aggressively to re-check is a protocol choice. |
| How is a check propagated to downstream consumers? | The proof is on-chain and replayable; subsequent verifiers can re-check or trust the prior verification depending on their threat model. |
On the "where does responsibility sit?" question
This is one of the working group's explicit questions: where should responsibility for a compliance check sit, given the choice between wallet, issuer, credential provider, policy engine, app, auditor, or user?
UPC's answer: the wire format makes responsibility expressible; it doesn't assign responsibility. The same UPC proof can be checked by a wallet (pre-flight), a smart contract (on-chain enforcement), a relayer (mempool screening), an indexer (after-the-fact audit), or an issuer's backend (re-verification before re-attestation). Whichever party in the stack chooses to call IAttestationVerifier.verify() is the party that bears responsibility for the result they got. Different protocols and different operators will pick different parties.
Worked example, not standard
The mapping on this page is UPP's choice with our reasoning. It is not part of the UPC standard. Other privacy systems are free to disagree row by row and still ship against the same IAttestationVerifier. If you want a different mapping, the wire supports it.