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:
- Sort tokens by address (token0 < token1)
- Calculate pool key from sorted tokens and fee
- Check if pool already exists
- Deploy new PrivacyPool contract with msg.sender as owner
- Register pool in mapping and array
- Emit
PoolCreatedevent
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 tokenassetB: Second tokenswapFee: 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 tierpool: Address of deployed poolowner: Address of pool creator (initial owner)
Fee Tiers
Common fee tiers (basis points over 1,000,000):
| Fee % | Basis Points | Use Case |
|---|---|---|
| 0.01% | 100 | Stablecoin pairs (USDC/USDT) |
| 0.05% | 500 | Correlated assets (wETH/stETH) |
| 0.3% | 3000 | Standard pairs (most common) |
| 1% | 10000 | Exotic/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
| Operation | Gas Cost | Notes |
|---|---|---|
| Create Pool | ~3.5M | One-time deployment |
| Query Pool | ~2K | View function |
| Get Pool Count | ~1K | View 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
| Feature | PrivacyPoolFactory | Uniswap V2 Factory |
|---|---|---|
| Token Sorting | Automatic | Automatic |
| Duplicate Prevention | Yes | Yes |
| Encrypted Reserves | Yes | No |
| Fee Tiers | Configurable | Fixed 0.3% |
| Hook System | Yes | No |
| Pool Ownership | Creator | Factory |
Related Contracts
- PrivacyPool - The pool contract deployed by factory
- Smart Contracts - Contract architecture overview
- Hooks System - Extensibility via hooks
Contract Source
View the complete source code: PrivacyPoolFactory.sol