UPP — Universal Private PoolSDK Reference

Fee System

Flat fees in UPD with per-transaction EIP-3009 authorization — no standing approvals, no percentage-based charges.

Fee System

UPP uses flat fees denominated in UPD (pegged to 1).Unlikepercentagebasedmodels(e.g.,Railguns0.251). Unlike percentage-based models (e.g., Railgun's 0.25% each way), the fee is the same whether you transfer 100 or $10M.

Fee Schedule

OperationSNARK FeeSTARK Fee
Shield (deposit)Configurable ($0 default)Configurable ($0 default)
Unshield (withdraw)Configurable ($0 default)Configurable ($0 default)
Transfer$1 UPD$5 UPD
Merge$0.50 UPD$2.50 UPD
JoinSplit / MergeTransfer$1 UPD$5 UPD
Swap fill$1 UPD
RagequitAlways freeAlways free

Ragequit is always free

The anti-censorship escape hatch (ragequit) never charges fees. If ASPs refuse to include your origin, you can always withdraw to your original address at zero cost.

All amounts are admin-configurable via governance. Fees use uint96 (sufficient for up to ~79B tokens with 18 decimals).

Fee Authorization

Fees are paid in UPD (or another configured ERC20). There are two payment paths:

Path 1: Self-pay (EIP-3009)

The user signs a one-time EIP-3009 receiveWithAuthorization for the exact fee amount. No standing approval is needed — each transaction requires a fresh signature.

// The SDK handles this automatically via the encode-tx helpers.
// The user signs exactly $1 per transfer — can't be overcharged.

Why EIP-3009?

  • No approve(pool, max) — eliminates the attack vector where an admin raises fees and drains users
  • Exact amount — user signs for precisely the fee, not an open-ended allowance
  • Single-use nonce — cannot be replayed
  • Time-bound — expires in minutes
  • Front-run safe — only the pool contract can execute the authorization

Path 2: Relayer pays

For fully private transfers via a relayer, the relayer fronts the fee using its own standing approval. The relayer gets reimbursed atomically through a withdraw-to-relayer proof bundled in the same transaction via UPPRouter.batch().

The user never signs any fee authorization — full privacy is preserved.

Path 3: Note-based (future)

Users can pay fees from shielded UPD notes by generating a withdraw proof targeting the pool treasury. This is bundled atomically with the main operation — no public wallet link at all.

Using Fees in the SDK

Every pool operation accepts an optional feeData parameter (default: empty bytes = no fee).

Transaction Encoding Helpers

The SDK provides encode*Tx functions that abstract away the ABI struct encoding and feeData parameter:

import {
  encodeShieldTxFromBuildData,
  encodeTransferTxFromBuildData,
  encodeWithdrawTxFromBuildData,
  encodeSwapFillTxFromBuildData,
} from '@permissionless-technologies/upp-sdk'

These return { abi, functionName, args } objects ready for viem's writeContractAsync:

// Shield example
const shieldData = await build({ amount: parseUnits('100', 18), originAddress: address })

await writeContractAsync({
  address: poolAddress,
  ...encodeShieldTxFromBuildData(shieldData),
})

// Transfer example
const transferData = await buildTransfer({ amount, to: recipientAddress })

await writeContractAsync({
  address: poolAddress,
  ...encodeTransferTxFromBuildData(transferData),
})

Available Encoders

From hook build data (convenience wrappers):

FunctionOperationSource Hook
encodeShieldTxFromBuildData()Shield (SNARK)useShield().build()
encodeTransferTxFromBuildData()Transfer (SNARK)usePoolTransfer().build()
encodeTransferTxFromSplitData()Split note (self-transfer)useSwap().buildNoteSplit()
encodeWithdrawTxFromBuildData()Withdraw (SNARK)useWithdraw().build()
encodeSwapOrderTxFromBuildData()Place swap orderuseSwap().buildPlaceOrder()
encodeSwapFillTxFromBuildData()Fill swap orderuseSwap().buildFillOrder()
encodeSwapClaimTxFromBuildData()Claim swap proceedsuseSwap().buildClaimOrder()
encodeSwapCancelTxFromBuildData()Cancel swap orderuseSwap().buildCancelOrder()

Low-level encoders (struct-shaped input):

FunctionOperation
encodeShieldTx()Shield (SNARK)
encodeShieldSTARKTx()Shield (STARK)
encodeTransferTx()Transfer (SNARK)
encodeTransferSTARKTx()Transfer (STARK)
encodeWithdrawTx()Withdraw (SNARK)
encodeWithdrawSTARKTx()Withdraw (STARK)
encodeMergeTx()Merge
encodeSwapOrderTx()Swap order
encodeSwapFillTx()Swap fill
encodeSwapClaimTx()Swap claim
encodeSwapCancelTx()Swap cancel

All functions that accept feeData default it to '0x' (no fee) when omitted.

Fee Configuration (Admin)

Fees are configured on-chain by the pool admin:

// Set SNARK fees: $1 transfer, $0.50 merge
await pool.setSnarkFees(parseUnits('1', 18), parseUnits('0.5', 18))

// Set STARK fees: $5 transfer, $2.50 merge
await pool.setStarkFees(parseUnits('5', 18), parseUnits('2.5', 18))

// Set the fee token (UPD address)
await pool.setFeeToken(UPD_TOKEN_ADDRESS)

// Withdraw accumulated fees
await pool.withdrawAccumulatedFees(treasuryAddress)

Reading Fee Config

const feeConfig = await pool.read.getFeeConfig()
// Returns: { feeTokenAddr, shieldFee, unshieldFee, snarkTransferFee,
//            snarkMergeFee, starkTransferFee, starkMergeFee, swapFillFee }

On this page