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
euint64values - Swap amounts encrypted end-to-end
- No public visibility of trading volumes or liquidity
Extensible Hook System
- Optional
IPrivacyPoolSwapHookfor swap events - Optional
IPrivacyPoolLiquidityHookfor 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 AproofA: Zero-knowledge proof for amount AamountBExt: Encrypted amount of token BproofB: 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:
- Decrypt and validate encrypted amounts
- Transfer tokens to pool via
confidentialTransferFrom - Initialize encrypted reserves
- Trigger liquidity hook
- Emit
LiquiditySeededevent
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 addproofA: Zero-knowledge proofamountBExt: Encrypted amount of token B to addproofB: 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 swapproofIn: Zero-knowledge proof for amountrecipient: Address to receive output tokenshookContext: Optional context data for hooks
Returns:
requestID: Decryption request ID for settlement
Process:
- Pull encrypted tokens from caller
- Call
beforeSwaphook (if configured) - Calculate output using constant product formula
- Request decryption from fhEVM oracle
- Store pending swap with encrypted state
- Emit
SwapDecryptionRequestedevent
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 IDcleartexts: Decrypted values from oracledecryptionProof: Cryptographic proof of decryption
Process:
- Verify decryption proof with
FHE.checkSignatures - Compute final output amount from decrypted denominator
- Update encrypted reserves
- Transfer output tokens to recipient
- Call
afterSwaphook (if configured) - Emit
SwapSettledandSwapExecutedevents - 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 implementingIPrivacyPoolSwapHookswapData: Persistent configuration data for swap hookliquidityHook: Contract implementingIPrivacyPoolLiquidityHookliquidityData: 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 onlyconfigureHooks(): 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
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:
| Operation | Gas Cost | Notes |
|---|---|---|
| Bootstrap | ~800K | One-time initialization |
| Contribute Liquidity | ~500K | Additional liquidity |
| Initiate Swap | ~600K | Request decryption |
| Settle Swap | ~400K | Oracle settlement |
| Configure Hooks | ~100K | One-time setup |
Related Contracts
- PrivacyPoolFactory - Deploy new pools
- Hooks System - Extensibility system
- Smart Contracts - Contract architecture overview
Contract Source
View the complete source code: PrivacyPool.sol