Skip to main content

PrivacyPoolFactory

Overview

PrivacyPoolFactory is a minimal factory contract for deploying confidential AMM pools with deterministic token ordering. Inspired by Uniswap v4's factory pattern, it ensures that each token pair can only have one pool per fee tier, preventing fragmented liquidity.

The factory automatically sorts tokens by address to maintain consistency, so creating a pool with (tokenA, tokenB) is equivalent to (tokenB, tokenA).

Key Features

Deterministic Pool Creation

  • Canonical pool addresses for each token pair + fee combination
  • Automatic token address sorting
  • Prevention of duplicate pools

Pool Discovery

  • Query existing pools by token pair and fee
  • List all created pools
  • Get pool count

Owner Control

  • Pool creator becomes the initial owner
  • Owner can bootstrap pool and configure hooks
  • Ownership transferable via OpenZeppelin Ownable

Contract Architecture

State Variables

// Pool registry: poolKey => pool address
mapping(bytes32 => address) public getPool;

// All deployed pools
address[] public allPools;

The pool key is calculated as:

bytes32 key = keccak256(abi.encodePacked(token0, token1, swapFee));

Core Functions

Create Pool

function createPool(
IERC7984 assetA,
IERC7984 assetB,
uint24 swapFee
) external returns (address pool)

Purpose: Deploy a new confidential AMM pool for a token pair

Parameters:

  • assetA: First confidential ERC20 token (IERC7984)
  • assetB: Second confidential ERC20 token (IERC7984)
  • swapFee: Swap fee in basis points over 1,000,000 (e.g., 3000 = 0.3%)

Returns:

  • pool: Address of the newly deployed PrivacyPool contract

Requirements:

  • Tokens must be different addresses
  • Neither token can be the zero address
  • Pool for this pair + fee must not already exist
  • Fee must be less than 1,000,000 (100%)

Process:

  1. Sort tokens by address (token0 < token1)
  2. Calculate pool key from sorted tokens and fee
  3. Check if pool already exists
  4. Deploy new PrivacyPool contract with msg.sender as owner
  5. Register pool in mapping and array
  6. Emit PoolCreated event

Example:

const factory = await ethers.getContractAt(
"PrivacyPoolFactory",
FACTORY_ADDRESS
);

// Create pool with 0.3% fee (3000 basis points over 1M)
const tx = await factory.createPool(tokenA.address, tokenB.address, 3000);

const receipt = await tx.wait();
const event = receipt.events.find((e) => e.event === "PoolCreated");
const poolAddress = event.args.pool;

console.log(`Pool created at: ${poolAddress}`);

Query Pool Address

function poolFor(
IERC7984 assetA,
IERC7984 assetB,
uint24 swapFee
) external view returns (address)

Purpose: Get the pool address for a specific token pair and fee tier

Parameters:

  • assetA: First token
  • assetB: Second token
  • swapFee: Fee tier

Returns:

  • Pool address if exists, otherwise address(0)

Example:

const poolAddress = await factory.poolFor(tokenA.address, tokenB.address, 3000);

if (poolAddress === ethers.ZeroAddress) {
console.log("Pool does not exist");
} else {
console.log(`Pool address: ${poolAddress}`);
}

Get Pool Count

function allPoolsLength() external view returns (uint256)

Purpose: Get the total number of pools created by this factory

Returns:

  • Total pool count

Example:

const count = await factory.allPoolsLength();
console.log(`Total pools: ${count}`);

// Iterate through all pools
for (let i = 0; i < count; i++) {
const poolAddress = await factory.allPools(i);
console.log(`Pool ${i}: ${poolAddress}`);
}

Events

PoolCreated

event PoolCreated(
address indexed assetA,
address indexed assetB,
uint24 swapFee,
address pool,
address indexed owner
);

Emitted: When a new pool is successfully created

Parameters:

  • assetA: First token (sorted)
  • assetB: Second token (sorted)
  • swapFee: Fee tier
  • pool: Address of deployed pool
  • owner: Address of pool creator (initial owner)

Fee Tiers

Common fee tiers (basis points over 1,000,000):

Fee %Basis PointsUse Case
0.01%100Stablecoin pairs (USDC/USDT)
0.05%500Correlated assets (wETH/stETH)
0.3%3000Standard pairs (most common)
1%10000Exotic/volatile pairs

Example:

// Stablecoin pool with 0.01% fee
await factory.createPool(confidentialUSDC, confidentialUSDT, 100);

// Standard pool with 0.3% fee
await factory.createPool(confidentialWETH, confidentialDAI, 3000);

// High volatility pool with 1% fee
await factory.createPool(confidentialToken, confidentialMeme, 10000);

Integration Guide

Complete Pool Deployment Flow

import { ethers } from "ethers";
import { getFhevmInstance } from "fhevmjs";

async function deployAndInitializePool() {
// 1. Get factory contract
const factory = await ethers.getContractAt(
"PrivacyPoolFactory",
FACTORY_ADDRESS
);

// 2. Check if pool already exists
let poolAddress = await factory.poolFor(tokenA.address, tokenB.address, 3000);

if (poolAddress === ethers.ZeroAddress) {
// 3. Create new pool
console.log("Creating new pool...");
const tx = await factory.createPool(tokenA.address, tokenB.address, 3000);
const receipt = await tx.wait();

const event = receipt.events.find((e) => e.event === "PoolCreated");
poolAddress = event.args.pool;
console.log(`Pool created at: ${poolAddress}`);
} else {
console.log(`Pool already exists at: ${poolAddress}`);
}

// 4. Get pool contract
const pool = await ethers.getContractAt("PrivacyPool", poolAddress);

// 5. Initialize fhEVM
const fhevm = await getFhevmInstance();

// 6. Approve tokens (set pool as operator)
await tokenA.approveOperator(pool.address);
await tokenB.approveOperator(pool.address);

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

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

// 8. Bootstrap pool with liquidity
console.log("Bootstrapping pool...");
const bootstrapTx = await pool.bootstrap(
encryptedA.handles[0],
encryptedA.inputProof,
encryptedB.handles[0],
encryptedB.inputProof
);
await bootstrapTx.wait();
console.log("Pool initialized!");

return poolAddress;
}

Query All Pools

async function getAllPools() {
const factory = await ethers.getContractAt(
"PrivacyPoolFactory",
FACTORY_ADDRESS
);

const count = await factory.allPoolsLength();
const pools = [];

for (let i = 0; i < count; i++) {
const poolAddress = await factory.allPools(i);
const pool = await ethers.getContractAt("PrivacyPool", poolAddress);

const assetA = await pool.assetA();
const assetB = await pool.assetB();
const swapFee = await pool.swapFee();

pools.push({
address: poolAddress,
assetA,
assetB,
swapFee: swapFee.toString(),
feePercent: (Number(swapFee) / 10000).toFixed(2) + "%",
});
}

return pools;
}

// Usage
const pools = await getAllPools();
console.table(pools);

Security Considerations

Deterministic Addresses

The factory ensures consistent pool addresses through token sorting. This prevents:

  • Duplicate pools for the same pair
  • Liquidity fragmentation
  • Confusion about canonical pool addresses

Ownership Model

The pool creator (msg.sender) becomes the initial owner with these privileges:

  • Bootstrap pool with initial liquidity
  • Configure hooks
  • Transfer ownership

Important: Pool owners should:

  • Bootstrap pools promptly to prevent griefing
  • Configure hooks carefully (they have access to encrypted amounts)
  • Consider transferring ownership to a DAO or multisig

No Permissioned Creation

Anyone can create a pool for any token pair. This is intentional for:

  • Permissionless innovation
  • Censorship resistance
  • Open participation

However, users should verify:

  • Token contracts are legitimate
  • Pool has sufficient liquidity
  • Fee tier is appropriate

Gas Costs

OperationGas CostNotes
Create Pool~3.5MOne-time deployment
Query Pool~2KView function
Get Pool Count~1KView function

Error Messages

"PrivacyPoolFactory: pool-exists"
// Attempting to create duplicate pool

"PrivacyPoolFactory: identical-assets"
// Token addresses are the same

"PrivacyPoolFactory: zero-asset"
// One or both tokens are zero address

Frontend Integration

React Hook Example

import { useCallback } from "react";
import { ethers } from "ethers";

export function usePoolFactory() {
const createPool = useCallback(
async (tokenA: string, tokenB: string, fee: number) => {
const factory = await ethers.getContractAt(
"PrivacyPoolFactory",
FACTORY_ADDRESS
);

const tx = await factory.createPool(tokenA, tokenB, fee);
const receipt = await tx.wait();

const event = receipt.events?.find((e) => e.event === "PoolCreated");
return event?.args?.pool;
},
[]
);

const getPool = useCallback(
async (tokenA: string, tokenB: string, fee: number) => {
const factory = await ethers.getContractAt(
"PrivacyPoolFactory",
FACTORY_ADDRESS
);

return await factory.poolFor(tokenA, tokenB, fee);
},
[]
);

const getAllPools = useCallback(async () => {
const factory = await ethers.getContractAt(
"PrivacyPoolFactory",
FACTORY_ADDRESS
);

const count = await factory.allPoolsLength();
const addresses = [];

for (let i = 0; i < count; i++) {
addresses.push(await factory.allPools(i));
}

return addresses;
}, []);

return { createPool, getPool, getAllPools };
}

Comparison with Uniswap

FeaturePrivacyPoolFactoryUniswap V2 Factory
Token SortingAutomaticAutomatic
Duplicate PreventionYesYes
Encrypted ReservesYesNo
Fee TiersConfigurableFixed 0.3%
Hook SystemYesNo
Pool OwnershipCreatorFactory

Contract Source

View the complete source code: PrivacyPoolFactory.sol