Skip to main content

Universal Router

The Universal Router is the coordination contract that discovers confidential pools, validates swap paths, and safely creates new pools on demand. It complements the pool contracts by acting as the single source of truth for routing metadata, while leaving encrypted executions to each PrivacyPool.

Key difference vs. public AMMs: encrypted inputs are bound to a specific pool address in fhEVM. For that reason the router never proxies swaps or liquidity calls—it focuses on orchestration, not execution.

Responsibilities

  • Deterministic pool creation via PrivacyPoolFactory
  • Pool registry lookups (getPool, poolExists)
  • Path validation and quoting for multi-hop routes
  • Safety checks around token ordering and fee tiers
  • Emission of canonical PoolCreated events for indexers

Capabilities Overview

FeatureDescription
Pool deploymentCalls factory.createPool and returns the deployed pool address.
Path discoveryValidates the (path, fees) tuple and returns the concrete pool addresses for every hop.
Pool existence checksLightweight guards for frontends and scripts before presenting an action to a user.
Helper utilitiesLeverages UniversalRouterHelper to reuse path validation logic off-chain and on-chain.

The contract inherits SepoliaConfig to gain access to fhEVM configuration and ReentrancyGuard for safety during pool creation bursts.

On-chain API

FunctionSignatureNotes
createPoolfunction createPool(IERC7984 tokenA, IERC7984 tokenB, uint24 fee) external returns (address pool)Deploys a new pool through the factory and emits PoolCreated. No liquidity is added—call bootstrap on the returned pool.
getPoolfunction getPool(IERC7984 tokenA, IERC7984 tokenB, uint24 fee) external view returns (address pool)Returns the deterministic address or address(0) if the pair has not been deployed for that fee tier.
quotePathfunction quotePath(address[] calldata path, uint24[] calldata fees) external view returns (address[] memory pools)Validates the entire route and returns the ordered pool list; reverts with InvalidPath/PoolNotFound when a hop is missing.
poolExistsfunction poolExists(IERC7984 tokenA, IERC7984 tokenB, uint24 fee) external view returns (bool exists)Gas-cheap existence probe for dashboards or bots.
isValidPathfunction isValidPath(address[] calldata path, uint24[] calldata fees) external view returns (bool valid)Boolean-only validation helper for interfaces that prefer not to bubble custom errors.

All revert reasons are intentionally explicit (InvalidPath, InvalidFeeLength, PoolNotFound, ZeroAddress) to improve UX logging when routing pipelines fail fast.

Deployment & Bootstrap Flow

  1. Create the pool through the router
    address pool = router.createPool(lunToken, usdcToken, 3000);
  2. Frontends store the emitted PoolCreated event so liquidity UIs and analytics know the canonical pool address.
  3. Bootstrap directly on the pool using encrypted inputs that are generated for the pool address:
    const enc = await fhevm.createEncryptedInput(pool, providerAddress);
    enc.add64(bootstrapAmount);
    const encrypted = await enc.encrypt();

    await privacyPool.bootstrap(
    encrypted.handles[0],
    encrypted.inputProof,
    encrypted.handles[1],
    encrypted.inputProof,
    lpRecipient,
    "0x"
    );
  4. Subsequent liquidity and swap operations continue to call the pool, never the router, ensuring encrypted handles remain valid.

Path Discovery & Validation

Use the read-only helpers to guarantee that every hop in a route has an existing pool and fee tier.

const path = [LUN, USDC, eBTC];
const fees = [3000, 3000];

const pools = await router.quotePath(path, fees); // throws if a hop is missing
console.log("Pool sequence:", pools);

const isValid = await router.isValidPath(path, fees); // boolean guard

Under the hood the router relies on UniversalRouterHelper.validatePath to enforce:

  • path.length >= 2
  • fees.length === path.length - 1
  • No zero addresses
  • Consecutive tokens match between hops

These checks can also be reused off-chain by importing the helper library in scripts.

Single-Hop Swap Workflow

While the router cannot execute encrypted swaps, it still plays a role in operator UX:

  1. Fetch the pool address via getPool(tokenIn, tokenOut, fee).
  2. Generate encrypted input handles that target that pool.
  3. Call swapExactAForB or swapExactBForA on the pool and capture the SwapDecryptionRequested event.
  4. Monitor the oracle settlement and emit UI notifications once SwapSettled fires.

Example helper snippet:

const pool = await router.getPool(LUN, USDC, 3000);
if (pool === ethers.ZeroAddress) throw new Error("Pool missing");

const encIn = await fhevm.createEncryptedInput(pool, signer.address);
encIn.add64(ethers.parseUnits("1000", 6));
const encrypted = await encIn.encrypt();

await privacyPool__factory.connect(pool, signer).swapExactAForB(
encrypted.handles[0],
encrypted.inputProof,
recipient,
"0x"
);

Multi-Hop Routing Strategy

  • Use quotePath to retrieve every pool address in the route.
  • Execute each hop sequentially, waiting for settlement before forwarding the encrypted output of hop n to hop n+1.
  • Keep contextual metadata (deadline, hook context, masking parameters) in your orchestration layer because the router does not store per-route state.
const pools = await router.quotePath(path, fees);
for (const pool of pools) {
await waitForSettlement(
await privacyPool__factory.connect(pool, signer).swapExactAForB(
currentHopHandle,
currentHopProof,
signer.address,
"0x"
)
);
// derive the handle for the next hop from the oracle output...
}

Operational Considerations

  • **Encrypted handles are pool-bound.**Never reuse encrypted amounts generated for the router or a different pool.
  • **Multi-hop is asynchronous.**Each hop must settle before proceeding, so orchestrators should persist state between steps.
  • **Slippage management is custom.**Because the router cannot preview plaintext reserves, enforce slippage bounds off-chain by simulating expected outputs and comparing against oracle results.
  • **Factory ownership.**Pool creation rights follow whatever ownership the factory exposes (typically the deployer or DAO). Ensure governance controls align with the DAO model.

Event Reference

EventParametersUsage
PoolCreated(address pool, address tokenA, address tokenB, uint24 fee)Emitted after every successful call to createPool.Index new pools, kick off bootstrap workflows, or refresh analytics caches.

Integration Checklist

Sepolia Testnet Deployment

The UniversalRouter redeployment is pending. The latest PrivacyPoolFactory lives at 0x325E2fC63CD617C1A09bA41f6fD9bbe5eFc074d8 on Sepolia and will be linked once the new router address is finalized. Refer to Deployed Addresses for updates.

Integration Steps

  1. Connect to the Router: Once redeployed, use the router address published in Deployed Addresses. The router will reference the factory at 0x325E2fC63CD617C1A09bA41f6fD9bbe5eFc074d8.

  2. ABI Integration: Mirror the router ABI in your SDK or reuse the generated TypeChain types from the Hardhat deployment.

  3. Pool Discovery: Always call router.poolExists(tokenA, tokenB, fee) before presenting swap or liquidity actions to end-users to verify the pool exists.

  4. Event Monitoring: When orchestrating swaps, persist each pending requestID from pool events and subscribe to the oracle settlement events for asynchronous completion.

  5. Documentation: Reference the complete lifecycle documentation in PrivacyPool, Liquidity Provision, and Swap Mechanism for detailed implementation guidance.

Contract Addresses Reference

For the complete list of deployed contracts, token addresses, and pool addresses, see Deployed Addresses.

By keeping the router focused on discovery and path validation while pools handle encrypted execution, Lunarys preserves the privacy guarantees of fhEVM without sacrificing developer ergonomics.