Skip to main content

PrivacyPool

Overview

PrivacyPool is a confidential Automated Market Maker (AMM) that enables private swaps between two confidential ERC20 tokens (IERC7984). All reserve accounting is performed on encrypted integers using Zama's Fully Homomorphic Encryption (FHE), keeping balances and liquidity completely private.

The pool uses a constant product formula (x * y = k) similar to Uniswap v2, but with all computations done on encrypted values. This ensures that:

  • Trading amounts remain private
  • Pool reserves are encrypted
  • Liquidity positions are confidential
  • Only authorized parties can decrypt specific values

Key Features

Full Privacy

  • All reserves stored as encrypted euint64 values
  • Swap amounts encrypted end-to-end
  • No public visibility of trading volumes or liquidity

Extensible Hook System

  • Optional IPrivacyPoolSwapHook for swap events
  • Optional IPrivacyPoolLiquidityHook for liquidity changes
  • Enable advanced features like LP tokens, fees distribution, and analytics

Efficient FHE Operations

  • Optimized encrypted arithmetic operations
  • Masked decryption to prevent information leakage
  • Asynchronous settlement via fhEVM oracle

Fair Pricing

  • Constant product AMM formula: x * y = k
  • Configurable swap fees (basis points over 1,000,000)
  • No front-running due to encrypted order flow

Contract Architecture

State Variables

// Immutable configuration
IERC7984 public immutable assetA;
IERC7984 public immutable assetB;
uint24 public immutable swapFee; // Fee in basis points

// Encrypted reserves
euint64 private reserveA;
euint64 private reserveB;

// Pool status
bool public reservesInitialized;
uint32 public lastUpdate;

Structs

PendingSwap

Stores swap state between initiation and settlement:

struct PendingSwap {
bool exists;
bool aForB; // Direction: A→B or B→A
address caller;
address recipient;
euint64 reserveInBefore; // Encrypted reserve snapshot
euint64 reserveOutBefore;
euint64 newReserveIn;
euint128 maskedNumerator; // Masked constant product
bytes hookContext; // Context for hooks
bytes32 amountInHandle;
bytes32 maskedDenominatorHandle;
}

Core Functions

Bootstrap Pool

function bootstrap(
externalEuint64 amountAExt,
bytes calldata proofA,
externalEuint64 amountBExt,
bytes calldata proofB
) external onlyOwner nonReentrant

Purpose: Initialize the pool with encrypted liquidity (one-time operation)

Parameters:

  • amountAExt: Encrypted amount of token A
  • proofA: Zero-knowledge proof for amount A
  • amountBExt: Encrypted amount of token B
  • proofB: Zero-knowledge proof for amount B

Requirements:

  • Can only be called once by owner
  • Owner must have set pool as operator on both tokens
  • Both amounts must be non-zero

Process:

  1. Decrypt and validate encrypted amounts
  2. Transfer tokens to pool via confidentialTransferFrom
  3. Initialize encrypted reserves
  4. Trigger liquidity hook
  5. Emit LiquiditySeeded event

Contribute Liquidity

function contributeLiquidity(
externalEuint64 amountAExt,
bytes calldata proofA,
externalEuint64 amountBExt,
bytes calldata proofB
) external ensureInitialized nonReentrant

Purpose: Add additional liquidity to an initialized pool

Note: This function does NOT mint LP tokens. External LP accounting can be implemented via hooks.

Parameters:

  • amountAExt: Encrypted amount of token A to add
  • proofA: Zero-knowledge proof
  • amountBExt: Encrypted amount of token B to add
  • proofB: Zero-knowledge proof

Requirements:

  • Pool must be initialized
  • Amounts should maintain pool ratio for balanced liquidity

Swap Token A for B

function swapExactAForB(
externalEuint64 amountInExt,
bytes calldata proofIn,
address recipient,
bytes calldata hookContext
) external ensureInitialized nonReentrant returns (uint256 requestID)

Purpose: Swap token A for token B with encrypted input amount

Parameters:

  • amountInExt: Encrypted amount of token A to swap
  • proofIn: Zero-knowledge proof for amount
  • recipient: Address to receive output tokens
  • hookContext: Optional context data for hooks

Returns:

  • requestID: Decryption request ID for settlement

Process:

  1. Pull encrypted tokens from caller
  2. Call beforeSwap hook (if configured)
  3. Calculate output using constant product formula
  4. Request decryption from fhEVM oracle
  5. Store pending swap with encrypted state
  6. Emit SwapDecryptionRequested event

AMM Formula:

k = reserveA * reserveB  (constant product)
amountIn_after_fee = amountIn * (1 - swapFee)
reserveA_new = reserveA + amountIn_after_fee
reserveB_new = k / reserveA_new
amountOut = reserveB - reserveB_new

Swap Token B for A

function swapExactBForA(
externalEuint64 amountInExt,
bytes calldata proofIn,
address recipient,
bytes calldata hookContext
) external ensureInitialized nonReentrant returns (uint256 requestID)

Same as swapExactAForB but swaps B → A.

Settle Swap

function settleSwap(
uint256 requestID,
bytes memory cleartexts,
bytes memory decryptionProof
) external nonReentrant

Purpose: Complete a swap after fhEVM oracle provides decrypted values

Parameters:

  • requestID: The pending swap request ID
  • cleartexts: Decrypted values from oracle
  • decryptionProof: Cryptographic proof of decryption

Process:

  1. Verify decryption proof with FHE.checkSignatures
  2. Compute final output amount from decrypted denominator
  3. Update encrypted reserves
  4. Transfer output tokens to recipient
  5. Call afterSwap hook (if configured)
  6. Emit SwapSettled and SwapExecuted events
  7. Clean up pending swap

Hook System

Configure Hooks

function configureHooks(
IPrivacyPoolSwapHook swapHook,
bytes calldata swapData,
IPrivacyPoolLiquidityHook liquidityHook,
bytes calldata liquidityData
) external onlyOwner

Purpose: Configure optional hook contracts for extended functionality

Parameters:

  • swapHook: Contract implementing IPrivacyPoolSwapHook
  • swapData: Persistent configuration data for swap hook
  • liquidityHook: Contract implementing IPrivacyPoolLiquidityHook
  • liquidityData: Persistent configuration data for liquidity hook

Swap Hooks

Interface:

interface IPrivacyPoolSwapHook {
function beforeSwap(
address caller,
bool aForB,
bytes32 encryptedAmountIn,
bytes calldata hookConfig,
bytes calldata swapContext
) external;

function afterSwap(
address caller,
address recipient,
bool aForB,
bytes32 encryptedAmountIn,
bytes32 encryptedAmountOut,
bytes calldata hookConfig,
bytes calldata swapContext
) external;
}

Use Cases:

  • Position NFT minting (track LP positions)
  • Fee distribution
  • Trading volume analytics
  • Referral systems
  • Dynamic fee adjustment

Liquidity Hooks

Interface:

interface IPrivacyPoolLiquidityHook {
function onLiquidityChanged(
address caller,
bytes32 encryptedReserveA,
bytes32 encryptedReserveB,
bytes calldata hookConfig,
bool initialBoot
) external;
}

Use Cases:

  • LP token minting/burning
  • Liquidity mining rewards
  • Reserve ratio monitoring
  • Impermanent loss tracking

Events

SwapDecryptionRequested

event SwapDecryptionRequested(
uint256 indexed requestID,
address indexed sender,
address indexed recipient,
bool aForB,
bytes32 maskedDenominatorHandle
);

Emitted when a swap is initiated and decryption is requested.

SwapSettled

event SwapSettled(
uint256 indexed requestID,
address indexed sender,
address indexed recipient,
bool aForB,
bytes32 amountOutHandle
);

Emitted when a swap is settled with decrypted output.

SwapExecuted

event SwapExecuted(
address indexed sender,
address indexed recipient,
bool aForB
);

Emitted when swap completes successfully.

LiquiditySeeded

event LiquiditySeeded(address indexed provider);

Emitted when pool is bootstrapped.

LiquidityAdjusted

event LiquidityAdjusted(address indexed caller, bool add);

Emitted when liquidity is added or removed.

HooksUpdated

event HooksUpdated(address swapHook, address liquidityHook);

Emitted when hooks are configured.

Security Considerations

Encrypted Reserves

All reserves are stored as euint64 and never exposed in plaintext. Only the pool contract can perform operations on these values.

Masked Decryption

The swap settlement uses a random mask to prevent leaking the exact reserve ratio during decryption. The mask is applied to both numerator and denominator of the constant product formula.

Reentrancy Protection

All external functions use OpenZeppelin's ReentrancyGuard to prevent reentrancy attacks.

Access Control

  • bootstrap(): Owner only
  • configureHooks(): Owner only
  • All other functions: Public (but encrypted data)

Front-Running Resistance

Since all amounts are encrypted, MEV bots cannot:

  • Front-run large swaps
  • Extract value through sandwich attacks
  • Predict price impact

Debug Functions

warning

These functions are for testing environments only and should be removed or disabled in production.

debugReservesForOwner

function debugReservesForOwner()
external
onlyOwner
returns (bytes32 handleA, bytes32 handleB)

Marks reserves as publicly decryptable and returns their handles. Use for off-chain inspection during testing.

getPendingSwapHandles

function getPendingSwapHandles(uint256 requestID)
external
view
returns (
bool exists,
bool aForB,
address caller,
address recipient,
bytes32 amountInHandle,
bytes32 maskedDenominatorHandle
)

Returns metadata about a pending swap request.

Example Usage

Initialize a Pool

// 1. Deploy pool via factory
const factory = await ethers.getContractAt(
"PrivacyPoolFactory",
FACTORY_ADDRESS
);
const tx = await factory.createPool(tokenA.address, tokenB.address, 3000); // 0.3% fee
await tx.wait();

// 2. Get pool address
const poolAddress = await factory.poolFor(tokenA.address, tokenB.address, 3000);
const pool = await ethers.getContractAt("PrivacyPool", poolAddress);

// 3. Encrypt initial liquidity
const input = fhevm.createEncryptedInput(pool.address, owner.address);
input.add64(ethers.parseUnits("1000", 18)); // 1000 token A
const encryptedA = await input.encrypt();

const input2 = fhevm.createEncryptedInput(pool.address, owner.address);
input2.add64(ethers.parseUnits("1000", 18)); // 1000 token B
const encryptedB = await input2.encrypt();

// 4. Bootstrap pool
await pool.bootstrap(
encryptedA.handles[0],
encryptedA.inputProof,
encryptedB.handles[0],
encryptedB.inputProof
);

Execute a Swap

// 1. Encrypt swap amount
const input = fhevm.createEncryptedInput(pool.address, user.address);
input.add64(ethers.parseUnits("10", 18)); // Swap 10 tokens
const encrypted = await input.encrypt();

// 2. Initiate swap
const tx = await pool.swapExactAForB(
encrypted.handles[0],
encrypted.inputProof,
user.address,
"0x" // empty hook context
);
const receipt = await tx.wait();

// 3. Get request ID from event
const event = receipt.events.find((e) => e.event === "SwapDecryptionRequested");
const requestID = event.args.requestID;

// 4. Wait for oracle to settle (automatic)
// The fhEVM oracle will call settleSwap() when decryption is ready

Gas Costs

Approximate gas costs on Zama fhEVM:

OperationGas CostNotes
Bootstrap~800KOne-time initialization
Contribute Liquidity~500KAdditional liquidity
Initiate Swap~600KRequest decryption
Settle Swap~400KOracle settlement
Configure Hooks~100KOne-time setup

Contract Source

View the complete source code: PrivacyPool.sol