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 100 or $10M.
Fee Schedule
| Operation | SNARK Fee | STARK 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 | — |
| Ragequit | Always free | Always 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):
| Function | Operation | Source 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 order | useSwap().buildPlaceOrder() |
encodeSwapFillTxFromBuildData() | Fill swap order | useSwap().buildFillOrder() |
encodeSwapClaimTxFromBuildData() | Claim swap proceeds | useSwap().buildClaimOrder() |
encodeSwapCancelTxFromBuildData() | Cancel swap order | useSwap().buildCancelOrder() |
Low-level encoders (struct-shaped input):
| Function | Operation |
|---|---|
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 }