Skip to Content
ReferenceComparison with Base

Copied from docs/research/2026-10-07-comparison-with-base.md  at build time. Edit that file.

zx402: comparison with the Base implementation

FieldValue
Status2026-10-07
QuestionDoes the Cardano port keep the safety rules of the Base implementation?
ComparedThis repository at main, and the Privacy Pools rail on Base (the vendored 0xbow Privacy Pools contracts of that repository, and the facilitator and SDK code)
WhoThe lead of the build, and two independent reviewers that read the code of both repositories. One read the circuits and the validators. One read the payment flow and the privacy

1. Result

The protocol core is a sound port. Every public input of every proof is bound to chain data. Value is conserved in every action of the pool. No reader found a way to move value without a valid proof.

The port is not at the trust level of Base. Base runs the audited 0xbow contracts with keys from a public ceremony. The circuits and validators here are new code with keys that one party made, and nobody outside the project has reviewed them.

The readers found weaknesses outside the validators, in the SDK, in the relayer and in the trust in the operator. Five could cost money and are fixed. Section 4 lists all of them.

2. Rule by rule

S is the folder of the vendored Base contracts. V is contracts/lib/zx402. C is circuits/src.

RuleBaseCardanoResult
A note is Poseidon(value, label, Poseidon(nullifier, secret))Hashed by the contract at deposit, S/PrivacyPool.sol:98Hashed inside the insert proof, C/lib/note.circom:5-36, C/insert.circom:50-54Same rule
A note is spent onceA map of spent nullifier hashes, S/State.sol:123-129One trie root. The insert of the hash needs a proof that it is absent, V/pool/common.ak:132-142Same rule, new mechanism
Only real deposits enter the treeThe contract hashes the tree, S/State.sol:136-152An insert proof. The validator derives all 15 public inputs from the two datums, the queue and the spent deposit outputs, and forces the pool balance to grow by the credited sum, V/pool/insert.ak:29-77Same rule, new mechanism
A spend proves ownership, approval and the amountThe 0xbow withdraw circuitThe same checks with a fixed depth of 32 and three 64-bit range checks, C/spend.circomSame rule
A payment cannot be redirectedThe context binds the recipient, the fee recipient and the fee rate, S/PrivacyPool.sol:43-61, S/Entrypoint.sol:133-176The context binds the pool, every payout, the relayer key and an expiry. The validator checks the outputs in order, V/pool/settle.ak:25-106, V/encoding.ak:28-47Same rule, plus an expiry
Only approved deposits pay in privateOnly the latest association root, S/PrivacyPool.sol:59Only the root in the association output that the transaction reads, V/pool/common.ak:115-129Same rule
The depositor can always exit in publicA map from label to depositor, S/PrivacyPool.sol:132-150The label is a hash that includes the refund key, and that key must sign. The proof holds the tree membership, V/pool/ragequit.ak:23-42Same rule, new mechanism
Every public input is below the field modulusIn the generated verifier, S/verifiers/WithdrawalVerifier.solBefore any curve operation, V/groth16.ak:37-53Same rule
Who sees note secretsThe Base implementation’s server makes the proof and gets the secretsThe agent makes the proof. The relayer gets the proof and the payoutsStronger
Admin powerThe owner can upgrade the entry contract, S/Entrypoint.sol:308No upgrade. The admin can pause deposits and set bounded values, contracts/validators/config.akStronger
A second deposit with the same precommitmentRefused on chain, S/Entrypoint.sol:324-326Not refused on chain. The SDK prevents itWeaker
The start state of the poolFixed by the constructor, S/State.sol:79-94Written by the deployer and checked off chain in partWeaker
The proving keysFrom the 0xbow ceremony. The tree needs no keys at allOne party made them, and a forged insert proof can set any rootWeaker
Outside reviewThe core was auditedNoneMissing

3. The payment flow

Both products pay in two steps. The pool pays a key that is used once, and that key pays the seller with a payment that a stock x402 facilitator accepts.

The one-time key does not hide the pool. The chain shows that the pool funded it. It exists for two reasons. A stock seller cannot check or submit a pool transaction. And a new key for each payment keeps the payments of one agent apart.

WhoWhat they learn on BaseWhat they learn here
SellerA new payer key, the amount, the buyer’s IP address, and that the pool funded the keyThe same
Operator of the relayerEverything: note secrets, label, deposit, change, recipientIP address, time, the one-time address and the amount. No note, no label, no deposit
Operator of the indexerThe same operator: everythingIP address and the time of each sync. The SDK always downloads full lists
Anyone who reads the chainDeposit, payment to the key, payment to the sellerThe same shape. The link between deposit and payment is hidden by the pool, as well as the pool is used
The agent’s own chain providerNot usedThe deposit wallet, every one-time address and every seller payment

4. Findings

FindingNeeds a dishonest partyWhat happened
With a lost store and the same seed, the SDK used a note secret again. The new note carried a spent nullifier and was locked for goodNoFixed. The SDK syncs before a deposit and skips used secrets. Proven on Preprod with the store moved away
A copy of a deposit’s precommitment and refund key, absorbed first, took over the SDK’s record. The owner’s real note was then lockedA third party, at its own costFixed. The SDK follows the exact output of a deposit it sent itself
After a lost reply the relayer called a settlement failed although it could still land. The SDK then dropped its record of the change noteNoFixed. The relayer answers uncertain, and the SDK releases a note only on a known refusal
The SDK checked the protocol fee against a rate that the indexer reported. The chain bounds the fee only from below, so an operator of indexer and relayer could take most of a noteOperatorFixed. The SDK reads the rate from the chain with its own provider
The SDK accepted any deadline in a quote, so a relayer could keep a note reservedOperatorFixed. A quote must expire within 15 minutes of the tip
The start state of the pool is not enforced on chain. A deployer could plant a second root, a fee balance or a negative fee rateDeployerOpen. Before outside deposits
The proving keys come from one partyDeployerOpen. Before outside deposits
A settle without a named relayer leaves its surplus to whoever submits itNoBy design. The SDK always names the relayer
A public exit reveals the payments of its deposit by subtractionNoBy design, as on Base
The agent’s chain provider sees the whole pathProviderOpen. An agent needs its own node for privacy from the provider
After a lost store the one-time addresses repeatNoOpen. It links payments and loses nothing
A repeated x402 request after an unclear answer pays againNoOpen
The privacy defaults of SPEC 13.2 are not builtNoOpen
Ragequit needs the config output as a reference input, and the root history holds 16 rootsAdmin or a flood of InsertsOpen. It can delay, not take

5. What a user feels

BaseCardano today
Time for one payment7 to 15 seconds69 to 116 seconds on Preprod
Cost on top of the priceAbout a cent of gas, paid by the operatorAbout 1.2 ADA: 1 ADA for the relayer and 0.17 ADA for the second transaction
Smallest payment0.001 USDCAbout 1 ADA, the minimum output of the ledger
AssetUSDCADA
Pool actionsMany in one blockAbout one in each block
Not built hereMCP server, agent spend limits, sanctions screening, recovery of change notes from the seed, gasless deposit, payment memory per request

The Base numbers come from the documents of the Base implementation. The Cardano numbers are in measurements.md.

6. Limits of this check

  • The 0xbow circuit sources are not in the Base repository, only compiled artifacts. The Base side of the circuit rules was read from the contracts and the proof layout.
  • Nobody checked the Poseidon constants again. Tests compare the circuit library with the TypeScript library.
  • Nobody checked that the deployed keys match the circuit sources, or the setup itself.
  • The trie library was read only where the pool uses it.
  • These were reads of the code by the build team and its tools. They are not an audit.