PrivacyPool Architecture
This document describes the detailed architecture of the confidential AMM system.
Overview
The PrivacyPool system implements a constant product AMM (x * y = k) where all reserves and swap amounts remain encrypted throughout the entire lifecycle. The system uses Zama's fhEVM for Fully Homomorphic Encryption operations.
Architecture Diagram

The diagram illustrates the complete swap flow from user initiation through encrypted processing to final settlement.
System Components
PrivacyPool Contract
The core AMM contract implementing encrypted liquidity management.
State Variables:
IERC7984 public immutable assetA;
IERC7984 public immutable assetB;
uint24 public immutable swapFee;
euint64 private reserveA; // Encrypted reserve
euint64 private reserveB; // Encrypted reserve
bool public reservesInitialized;
Key Features:
- Encrypted reserve accounting
- Constant product formula on encrypted values
- Masked decryption for privacy preservation
- Asynchronous oracle-based settlement
- Extensible hook system
PrivacyPoolFactory Contract
Deploys and manages pool instances.
Functionality:
- Deterministic pool creation
- Token address sorting
- Pool registry
- Duplicate prevention
Pool Key:
bytes32 key = keccak256(abi.encodePacked(token0, token1, swapFee));
Hook Contracts
Optional external contracts extending pool functionality.
Interfaces:
IPrivacyPoolSwapHook: Called before and after swapsIPrivacyPoolLiquidityHook: Called on liquidity changes
Example: PositionNFT: Tracks LP positions as NFTs with encrypted liquidity amounts.
Complete Swap Flow
Phase 1: Initiation
User Side:
- User encrypts swap amount using fhevmjs
- User approves pool as token operator
- User calls
swapExactAForB()with encrypted amount
Contract Processing:
function swapExactAForB(
externalEuint64 amountInExt,
bytes calldata proofIn,
address recipient,
bytes calldata hookContext
) external returns (uint256 requestID) {
// 1. Validate inputs
// 2. Pull encrypted tokens from user
euint64 pulled = _pullIntoPool(assetA, amountIn);
// 3. Call beforeSwap hook (if configured)
if (hooks.swapHook != address(0)) {
hooks.swapHook.beforeSwap(msg.sender, true, pulledHandle, ...);
}
// 4. Calculate output (encrypted)
euint64 effectiveIn = _applyFee(pulled);
euint64 newReserveIn = FHE.add(reserveIn, effectiveIn);
// 5. Apply masked decryption
euint16 mask = FHE.randEuint16();
euint128 maskedNumerator = FHE.mul(k, mask);
euint128 maskedDenominator = FHE.mul(newReserveIn, mask);
// 6. Request oracle decryption
requestID = FHE.requestDecryption([maskedDenominator], settleSwap.selector);
// 7. Store pending swap
pendingSwaps[requestID] = PendingSwap({...});
}
State After Initiation:
- Input tokens are in pool
- Swap is pending oracle response
- All values remain encrypted
- User receives requestID
Phase 2: Oracle Processing
Off-Chain (Automatic):
- Oracle detects decryption request
- Oracle decrypts masked denominator using threshold cryptography
- Oracle generates cryptographic proof
- Oracle calls
settleSwap()callback
Security:
- Multiple oracle nodes participate
- Threshold signatures required
- No single point of decryption
- Proof verified on-chain
Phase 3: Settlement
Contract Processing:
function settleSwap(
uint256 requestID,
bytes memory cleartexts,
bytes memory decryptionProof
) external {
// 1. Load pending swap
PendingSwap storage pending = pendingSwaps[requestID];
// 2. Verify oracle proof
FHE.checkSignatures(requestID, cleartexts, decryptionProof);
// 3. Decode decrypted value
uint128 decryptedDenominator = abi.decode(cleartexts, (uint128));
// 4. Calculate final output (encrypted)
euint64 reserveOutNew = FHE.div(pending.maskedNumerator, decryptedDenominator);
euint64 amountOut = FHE.sub(pending.reserveOutBefore, reserveOutNew);
// 5. Update reserves
reserveA = pending.newReserveIn;
reserveB = reserveOutNew;
// 6. Transfer output tokens
assetB.confidentialTransfer(pending.recipient, amountOut);
// 7. Call afterSwap hook (if configured)
if (hooks.swapHook != address(0)) {
hooks.swapHook.afterSwap(caller, recipient, aForB, amountIn, amountOut, ...);
}
// 8. Call liquidity hook (if configured)
if (hooks.liquidityHook != address(0)) {
hooks.liquidityHook.onLiquidityChanged(caller, reserveA, reserveB, ...);
}
// 9. Emit events and cleanup
emit SwapExecuted(caller, recipient, aForB);
delete pendingSwaps[requestID];
}
Final State:
- Reserves updated (encrypted)
- Output tokens transferred to recipient
- Hooks notified
- Request cleaned up
Liquidity Management
Bootstrap (Initial Liquidity)
Process:
- Owner deploys pool via factory
- Owner approves tokens
- Owner encrypts initial amounts
- Owner calls
bootstrap() - Pool pulls encrypted tokens
- Reserves initialized
- Hook notified (if configured)
Code Flow:
function bootstrap(
externalEuint64 amountAExt,
bytes calldata proofA,
externalEuint64 amountBExt,
bytes calldata proofB
) external onlyOwner {
require(!reservesInitialized, "already initialized");
euint64 amountA = FHE.fromExternal(amountAExt, proofA);
euint64 amountB = FHE.fromExternal(amountBExt, proofB);
_pullIntoPool(assetA, amountA);
_pullIntoPool(assetB, amountB);
reserveA = amountA;
reserveB = amountB;
reservesInitialized = true;
_notifyLiquidityChange(true);
}
Contribute Liquidity
Process:
- LP approves tokens
- LP encrypts amounts
- LP calls
contributeLiquidity() - Pool pulls tokens
- Reserves updated
- Hook notified
Note: No LP tokens minted by default. Use hooks for LP accounting.
Hook Integration
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;
}
Execution Points:
beforeSwap: After tokens pulled, before calculationafterSwap: After settlement, before cleanup
Use Cases:
- Position tracking
- Fee distribution
- Volume analytics
- Custom validations
Liquidity Hooks
Interface:
interface IPrivacyPoolLiquidityHook {
function onLiquidityChanged(
address caller,
bytes32 encryptedReserveA,
bytes32 encryptedReserveB,
bytes calldata hookConfig,
bool initialBoot
) external;
}
Execution Points:
- After
bootstrap() - After
contributeLiquidity()
Use Cases:
- LP NFT minting
- Share calculation
- Rewards distribution
- Position updates
Example: PositionNFT Hook
Functionality:
- Mints NFT for each LP
- Stores encrypted liquidity amounts
- Updates positions on contributions
- Tracks ownership and shares
Implementation:
contract PositionNFT is IPrivacyPoolSwapHook, IPrivacyPoolLiquidityHook, ERC721 {
struct Position {
uint256 tokenId;
euint64 liquidityA;
euint64 liquidityB;
uint256 timestamp;
}
mapping(address => Position) public positions;
function onLiquidityChanged(
address caller,
bytes32 encryptedReserveA,
bytes32 encryptedReserveB,
bytes calldata hookConfig,
bool initialBoot
) external override {
if (initialBoot || positions[caller].tokenId == 0) {
_mintPosition(caller, encryptedReserveA, encryptedReserveB);
} else {
_updatePosition(caller, encryptedReserveA, encryptedReserveB);
}
}
}
Privacy Mechanisms
Masked Decryption
Problem: Decrypting denominator would reveal reserve ratio
Solution: Apply random mask before decryption
Process:
- Generate random mask
m - Compute
masked_numerator = k * m - Compute
masked_denominator = newReserveIn * m - Decrypt only
masked_denominator - Calculate
result = masked_numerator / decrypted_masked_denominator - Mask cancels out:
(k * m) / (newReserveIn * m) = k / newReserveIn
Result: Correct output without revealing reserves
Permission System
For Swap Amounts:
FHE.allow(amountIn, address(this)); // Pool contract
FHE.allow(amountIn, address(assetA)); // Token contract
FHE.allow(amountIn, trader); // Original sender
FHE.allow(amountOut, recipient); // Output recipient
For Reserves:
FHE.allowThis(reserveA); // Only pool contract
FHE.allowThis(reserveB); // Only pool contract
// Never granted to external addresses
For Hooks:
// Hooks receive bytes32 handles
// Can perform FHE operations
// Cannot decrypt without permission
bytes32 handle = FHE.toBytes32(encryptedValue);
hook.afterSwap(caller, recipient, aForB, handleIn, handleOut, ...);
Constant Product Formula
AMM Math (Encrypted)
Invariant:
k = reserveA * reserveB (constant product)
Swap Calculation:
For swap A → B:
1. effectiveIn = amountIn * (1 - fee)
2. newReserveA = reserveA + effectiveIn
3. newReserveB = k / newReserveA
4. amountOut = reserveB - newReserveB
FHE Implementation:
euint64 effectiveIn = _applyFee(amountIn);
euint64 newReserveIn = FHE.add(reserveIn, effectiveIn);
euint128 k = FHE.mul(
FHE.asEuint128(reserveIn),
FHE.asEuint128(reserveOut)
);
// Apply masking for privacy
// Request decryption
// Calculate output in settlement
Security Considerations
Reentrancy Protection
All external functions use nonReentrant modifier.
Access Control
bootstrap(): Owner onlyconfigureHooks(): Owner onlyswapExactAForB(): Public (but encrypted)swapExactBForA(): Public (but encrypted)settleSwap(): Public (called by oracle)
Front-Running Resistance
Since amounts are encrypted:
- MEV bots cannot see trade size
- Cannot calculate price impact
- Cannot sandwich attack
- Cannot front-run profitably
Oracle Security
- Threshold decryption (no single point)
- Cryptographic proof verification
- On-chain proof checking
- Request ID tracking
Gas Costs
| Operation | Approximate Gas | Notes |
|---|---|---|
| Pool Creation | ~3.5M | Via factory |
| Bootstrap | ~800K | One-time initialization |
| Contribute Liquidity | ~500K | Additional liquidity |
| Swap Initiation | ~600K | User transaction |
| Swap Settlement | ~400K | Oracle callback |
| Configure Hooks | ~100K | One-time setup |
Integration Examples
Creating a Pool
const factory = await ethers.getContractAt(
"PrivacyPoolFactory",
factoryAddress
);
const tx = await factory.createPool(tokenA, tokenB, 3000); // 0.3% fee
const receipt = await tx.wait();
const poolAddress = receipt.events.find((e) => e.event === "PoolCreated").args
.pool;
Adding Liquidity
const input = fhevm.createEncryptedInput(poolAddress, ownerAddress);
input.add64(ethers.parseUnits("1000", 18));
const encryptedA = await input.encrypt();
const input2 = fhevm.createEncryptedInput(poolAddress, ownerAddress);
input2.add64(ethers.parseUnits("1000", 18));
const encryptedB = await input2.encrypt();
await pool.bootstrap(
encryptedA.handles[0],
encryptedA.inputProof,
encryptedB.handles[0],
encryptedB.inputProof
);
Executing a Swap
const input = fhevm.createEncryptedInput(poolAddress, userAddress);
input.add64(ethers.parseUnits("10", 18));
const encrypted = await input.encrypt();
const tx = await pool.swapExactAForB(
encrypted.handles[0],
encrypted.inputProof,
userAddress,
"0x" // empty hook context
);
const receipt = await tx.wait();
const requestID = receipt.events.find(
(e) => e.event === "SwapDecryptionRequested"
).args.requestID;
// Oracle will automatically settle
Related Documentation
- PrivacyPool Contract - Complete contract reference
- PrivacyPoolFactory - Factory documentation
- Hooks System - Hook development guide
- Privacy Protocol - FHE deep dive