Skip to main content

Hook System

Overview

The Hook System provides extensibility points for PrivacyPool, allowing developers to implement custom logic that executes during swap and liquidity events. Hooks enable advanced features like LP position tracking, fee distribution, analytics, and integration with external protocols—all while maintaining privacy through encrypted data flows.

Hooks are inspired by Uniswap v4's hook architecture but adapted for fully homomorphic encryption (FHE) operations.

Key Concepts

What are Hooks?

Hooks are external smart contracts that implement predefined interfaces and are called by PrivacyPool at specific lifecycle points:

  • Swap Hooks: Called before and after swap execution
  • Liquidity Hooks: Called when liquidity is added or removed

Why Use Hooks?

Without Hooks (core PrivacyPool):

  • Basic AMM functionality
  • No LP token representation
  • No position tracking
  • No custom fee logic

With Hooks (extended capabilities):

  • LP position NFTs (PositionNFT)
  • Custom fee distributions
  • Volume-based rewards
  • Integration with lending protocols
  • Dynamic pricing strategies
  • On-chain analytics

Privacy Considerations

Hooks receive encrypted values as bytes32 handles, not plaintext amounts. This means:

  • Hooks can store and track encrypted data
  • Hooks can perform FHE operations
  • Hooks maintain pool privacy guarantees
  • Hooks cannot directly see amounts
  • Hooks cannot leak sensitive information

Hook Interfaces

IPrivacyPoolSwapHook

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;
}

Parameters:

  • caller: Address initiating the swap
  • recipient: Address receiving output tokens
  • aForB: Swap direction (true = A→B, false = B→A)
  • encryptedAmountIn: Encrypted input amount handle
  • encryptedAmountOut: Encrypted output amount handle (afterSwap only)
  • hookConfig: Persistent configuration set by pool owner
  • swapContext: Per-swap context data provided by caller

IPrivacyPoolLiquidityHook

interface IPrivacyPoolLiquidityHook {
function onLiquidityChanged(
address caller,
bytes32 encryptedReserveA,
bytes32 encryptedReserveB,
bytes calldata hookConfig,
bool initialBoot
) external;
}

Parameters:

  • caller: Address modifying liquidity
  • encryptedReserveA: Encrypted reserve A handle
  • encryptedReserveB: Encrypted reserve B handle
  • hookConfig: Persistent configuration set by pool owner
  • initialBoot: True if this is the bootstrap call

Hook Configuration

Setting Hooks

Only the pool owner can configure hooks:

function configureHooks(
IPrivacyPoolSwapHook swapHook,
bytes calldata swapData,
IPrivacyPoolLiquidityHook liquidityHook,
bytes calldata liquidityData
) external onlyOwner;

Example:

const pool = await ethers.getContractAt("PrivacyPool", poolAddress);

// Deploy hook contracts
const positionNFT = await deployContract("PositionNFT", [poolAddress]);
const feeDistributor = await deployContract("FeeDistributor", [poolAddress]);

// Configure hooks
await pool.configureHooks(
positionNFT.address, // Swap hook
ethers.AbiCoder.defaultAbiCoder().encode(
["uint256", "address"],
[10000, treasury.address] // Hook config: fee BPS, treasury
),
positionNFT.address, // Liquidity hook (same contract)
"0x" // Empty liquidity config
);

Hook Configuration Data

The hookConfig parameter allows pools to pass persistent configuration to hooks:

struct HookConfig {
IPrivacyPoolSwapHook swapHook;
IPrivacyPoolLiquidityHook liquidityHook;
bytes swapHookData; // Persistent config for swap hook
bytes liquidityHookData; // Persistent config for liquidity hook
}

Use Cases:

  • Fee percentages
  • Treasury addresses
  • Reward multipliers
  • Integration parameters

Example Implementations

1. Position NFT Hook

Track LP positions as NFTs with encrypted liquidity amounts.

contract PositionNFT is
IPrivacyPoolSwapHook,
IPrivacyPoolLiquidityHook,
ERC721
{
struct Position {
uint256 tokenId;
euint64 liquidityA;
euint64 liquidityB;
uint256 timestamp;
}

mapping(address => Position) public positions;
uint256 public nextTokenId = 1;

function onLiquidityChanged(
address caller,
bytes32 encryptedReserveA,
bytes32 encryptedReserveB,
bytes calldata hookConfig,
bool initialBoot
) external override {
require(msg.sender == address(pool), "unauthorized");

if (initialBoot) {
// Bootstrap - creator gets initial position
_mintPosition(caller, encryptedReserveA, encryptedReserveB);
} else {
// Update existing position or mint new
if (positions[caller].tokenId != 0) {
_updatePosition(caller, encryptedReserveA, encryptedReserveB);
} else {
_mintPosition(caller, encryptedReserveA, encryptedReserveB);
}
}
}

function beforeSwap(
address caller,
bool aForB,
bytes32 encryptedAmountIn,
bytes calldata hookConfig,
bytes calldata swapContext
) external override {
// Could implement swap-based position updates
}

function afterSwap(
address caller,
address recipient,
bool aForB,
bytes32 encryptedAmountIn,
bytes32 encryptedAmountOut,
bytes calldata hookConfig,
bytes calldata swapContext
) external override {
// Could implement fee accrual to positions
}

function _mintPosition(
address to,
bytes32 handleA,
bytes32 handleB
) internal {
uint256 tokenId = nextTokenId++;
_mint(to, tokenId);

euint64 liquidityA = FHE.asEuint64(handleA);
euint64 liquidityB = FHE.asEuint64(handleB);

FHE.allow(liquidityA, address(this));
FHE.allow(liquidityB, address(this));

positions[to] = Position({
tokenId: tokenId,
liquidityA: liquidityA,
liquidityB: liquidityB,
timestamp: block.timestamp
});
}

function _updatePosition(
address owner,
bytes32 handleA,
bytes32 handleB
) internal {
Position storage pos = positions[owner];

// Add to existing position
euint64 addedA = FHE.asEuint64(handleA);
euint64 addedB = FHE.asEuint64(handleB);

pos.liquidityA = FHE.add(pos.liquidityA, addedA);
pos.liquidityB = FHE.add(pos.liquidityB, addedB);

FHE.allow(pos.liquidityA, address(this));
FHE.allow(pos.liquidityB, address(this));
}
}

2. Fee Distribution Hook

Distribute swap fees to liquidity providers.

contract FeeDistributorHook is IPrivacyPoolSwapHook {
struct FeeConfig {
uint256 lpFeeBps; // Basis points to LPs
address treasury; // Protocol treasury
}

mapping(address => euint64) public pendingFees;

function afterSwap(
address caller,
address recipient,
bool aForB,
bytes32 encryptedAmountIn,
bytes32 encryptedAmountOut,
bytes calldata hookConfig,
bytes calldata swapContext
) external override {
FeeConfig memory config = abi.decode(hookConfig, (FeeConfig));

// Calculate LP fee share
euint64 amountIn = FHE.asEuint64(encryptedAmountIn);
euint64 fee = FHE.div(
FHE.mul(amountIn, uint64(config.lpFeeBps)),
uint64(10000)
);

// Accrue to all LPs proportionally
_distributeFees(fee, config.treasury);
}

function beforeSwap(
address caller,
bool aForB,
bytes32 encryptedAmountIn,
bytes calldata hookConfig,
bytes calldata swapContext
) external override {
// No-op
}

function _distributeFees(euint64 fee, address treasury) internal {
// Implementation: distribute to LP positions
// This would integrate with PositionNFT or similar
}

function claimFees(address lp) external {
euint64 fees = pendingFees[lp];
require(FHE.decrypt(FHE.gt(fees, FHE.asEuint64(0))), "no fees");

// Transfer fees to LP
pendingFees[lp] = FHE.asEuint64(0);
// ... transfer logic
}
}

3. Volume Tracker Hook

Track trading volume for rewards or analytics.

contract VolumeTrackerHook is IPrivacyPoolSwapHook {
struct UserVolume {
euint64 totalVolumeIn;
euint64 totalVolumeOut;
uint256 swapCount;
}

mapping(address => UserVolume) public userVolumes;
euint64 public totalPoolVolume;

function afterSwap(
address caller,
address recipient,
bool aForB,
bytes32 encryptedAmountIn,
bytes32 encryptedAmountOut,
bytes calldata hookConfig,
bytes calldata swapContext
) external override {
UserVolume storage vol = userVolumes[caller];

euint64 amountIn = FHE.asEuint64(encryptedAmountIn);
euint64 amountOut = FHE.asEuint64(encryptedAmountOut);

// Update user volume
vol.totalVolumeIn = FHE.add(vol.totalVolumeIn, amountIn);
vol.totalVolumeOut = FHE.add(vol.totalVolumeOut, amountOut);
vol.swapCount++;

// Update pool volume
totalPoolVolume = FHE.add(totalPoolVolume, amountIn);

FHE.allow(vol.totalVolumeIn, caller);
FHE.allow(vol.totalVolumeOut, caller);
FHE.allowThis(totalPoolVolume);
}

function beforeSwap(
address caller,
bool aForB,
bytes32 encryptedAmountIn,
bytes calldata hookConfig,
bytes calldata swapContext
) external override {
// No-op
}

function getUserVolumeHandles(address user)
external
view
returns (bytes32 volumeIn, bytes32 volumeOut)
{
UserVolume storage vol = userVolumes[user];
return (
FHE.toBytes32(vol.totalVolumeIn),
FHE.toBytes32(vol.totalVolumeOut)
);
}
}

4. Dynamic Fee Hook

Adjust fees based on pool conditions.

contract DynamicFeeHook is IPrivacyPoolSwapHook, IPrivacyPoolLiquidityHook {
euint64 public reserveA;
euint64 public reserveB;
uint256 public baseFee = 3000; // 0.3%
uint256 public volatilityMultiplier = 10000; // 1x

function onLiquidityChanged(
address caller,
bytes32 encryptedReserveA,
bytes32 encryptedReserveB,
bytes calldata hookConfig,
bool initialBoot
) external override {
// Track reserves to calculate imbalance
reserveA = FHE.asEuint64(encryptedReserveA);
reserveB = FHE.asEuint64(encryptedReserveB);

FHE.allowThis(reserveA);
FHE.allowThis(reserveB);
}

function beforeSwap(
address caller,
bool aForB,
bytes32 encryptedAmountIn,
bytes calldata hookConfig,
bytes calldata swapContext
) external override {
// Could validate swap size or apply dynamic fees
// For now, just track that swap is happening
}

function afterSwap(
address caller,
address recipient,
bool aForB,
bytes32 encryptedAmountIn,
bytes32 encryptedAmountOut,
bytes calldata hookConfig,
bytes calldata swapContext
) external override {
// Update internal state based on swap
// Could adjust volatilityMultiplier based on swap size
}

function getAdjustedFee() external view returns (uint256) {
return (baseFee * volatilityMultiplier) / 10000;
}
}

Hook Execution Flow

Swap Flow with Hooks

User calls swapExactAForB()

Pull tokens from user

beforeSwap() hook ← Hook can validate/track

Calculate output (encrypted)

Request decryption from oracle

Store pending swap

[Wait for oracle]

Oracle calls settleSwap()

Verify decryption proof

Transfer output to recipient

afterSwap() hook ← Hook can distribute fees

onLiquidityChanged() hook ← Reserves changed

Emit events

Liquidity Flow with Hooks

Owner calls bootstrap()

Pull tokens from owner

Set encrypted reserves

onLiquidityChanged() hook ← initialBoot = true

Emit LiquiditySeeded event

Best Practices

1. Gas Optimization

  • Minimize storage writes
  • Batch FHE operations when possible
  • Use view functions for queries
  • Cache frequently accessed data

2. Security

  • Validate msg.sender is the pool
  • Use reentrancy guards if calling external contracts
  • Be careful with permissions on hook data
  • Test thoroughly with encrypted values

3. Privacy

  • Never decrypt sensitive values in hooks
  • Don't emit unencrypted amounts in events
  • Use FHE operations for all comparisons
  • Allow proper permissions for encrypted values

4. Upgrade Safety

  • Make hooks upgradeable or replaceable
  • Version hook configurations
  • Document expected behavior clearly
  • Test hook interactions extensively

Hook Limitations

What Hooks CANNOT Do

Modify swap amounts

  • Hooks are called after amount calculation
  • Cannot dynamically adjust output

Block swaps directly

  • Cannot revert from hooks (will break pool)
  • Use external validation contracts instead

Access plaintext amounts

  • All values are encrypted
  • Must request decryption via oracle if needed

What Hooks CAN Do

Track encrypted data

  • Store positions, volumes, fees

Perform FHE operations

  • Add, subtract, multiply encrypted values

Integrate with other contracts

  • NFTs, governance, rewards

Emit events

  • For off-chain indexing

Testing Hooks

Unit Test Example

describe("PositionNFT Hook", () => {
it("should mint position NFT on bootstrap", async () => {
// Deploy pool with hook
const positionNFT = await deploy("PositionNFT", [pool.address]);
await pool.configureHooks(
positionNFT.address,
"0x",
positionNFT.address,
"0x"
);

// Bootstrap pool
await pool.bootstrap(encryptedA, proofA, encryptedB, proofB);

// Check NFT minted
const balance = await positionNFT.balanceOf(owner.address);
expect(balance).to.equal(1);

// Check position data
const position = await positionNFT.positions(owner.address);
expect(position.tokenId).to.equal(1);
});

it("should update position on liquidity contribution", async () => {
// Contribute more liquidity
await pool.contributeLiquidity(encryptedA2, proofA2, encryptedB2, proofB2);

// Position should be updated, not new NFT minted
const balance = await positionNFT.balanceOf(owner.address);
expect(balance).to.equal(1); // Still just 1 NFT

// Could decrypt and verify amounts increased
});
});

Contract Sources