Skip to main content

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

PrivacyPool Complete Flow

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 swaps
  • IPrivacyPoolLiquidityHook: Called on liquidity changes

Example: PositionNFT: Tracks LP positions as NFTs with encrypted liquidity amounts.

Complete Swap Flow

Phase 1: Initiation

User Side:

  1. User encrypts swap amount using fhevmjs
  2. User approves pool as token operator
  3. 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):

  1. Oracle detects decryption request
  2. Oracle decrypts masked denominator using threshold cryptography
  3. Oracle generates cryptographic proof
  4. 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:

  1. Owner deploys pool via factory
  2. Owner approves tokens
  3. Owner encrypts initial amounts
  4. Owner calls bootstrap()
  5. Pool pulls encrypted tokens
  6. Reserves initialized
  7. 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:

  1. LP approves tokens
  2. LP encrypts amounts
  3. LP calls contributeLiquidity()
  4. Pool pulls tokens
  5. Reserves updated
  6. 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 calculation
  • afterSwap: 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:

  1. Generate random mask m
  2. Compute masked_numerator = k * m
  3. Compute masked_denominator = newReserveIn * m
  4. Decrypt only masked_denominator
  5. Calculate result = masked_numerator / decrypted_masked_denominator
  6. 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 only
  • configureHooks(): Owner only
  • swapExactAForB(): 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

OperationApproximate GasNotes
Pool Creation~3.5MVia factory
Bootstrap~800KOne-time initialization
Contribute Liquidity~500KAdditional liquidity
Swap Initiation~600KUser transaction
Swap Settlement~400KOracle callback
Configure Hooks~100KOne-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