Integrate as a Router or Aggregator
Source liquidity from DualPool pools with the hook's quote surface, sizing rules, and failure modes.
Context
This guide is for aggregators, routers, solvers, and quote APIs sourcing liquidity from DualPool pools. The headline: execution is a completely ordinary v4 swap; only quoting is different. A DualPool holds zero persistent v4 liquidity between swaps, so onchain pool depth is not a routing signal: getSlot0 shows a price, but getLiquidity is ~0. Capacity is discovered through the hook's view functions.
Detect a DualPool
Discover hooks through the AllowlistedFactory (deployments): index its Deployed events, or enumerate allDeployments onchain. Verify provenance in either direction with factory.isFromFactory(hook) or hook.factory(); a hook the factory vouches for is a bit-exact build of the audited bytecode. For each hook, its pools are the PoolCreated events it emits (equivalently, the PoolManager's Initialize events filtered on the hook address).
Once you have a candidate hook, confirm it implements the ALF view interface (IALFHook.sol) via ERC-165:
bool isAlf = IERC165(address(key.hooks)).supportsInterface(type(IALFHook).interfaceId);A hook that passes carries the uniform IALFHook quote surface below, so you do not need to distinguish DualPool from other ALF strategies. DualPool-specific signals, if you want them, include livePools(poolId) and getDistribution(poolId).
The Quote Surface
All methods are view, intended to be called via staticcall:
| Method | Returns | Use it for |
|---|---|---|
getIndicativeQuote(key, zeroForOne, amountSpecified, hookData) | uint256 | Single-number indicative for ranking |
swapToPrice(key, zeroForOne, amountSpecified, sqrtPriceLimitX96, hookData) | (amountIn, amountOut) | Price-bounded fill simulation for split planning |
getReserves(key) | (token0, token1) | Total economic assets under management |
getEffectiveLiquidity(key) | (token0, token1) | Immediately swappable liquidity |
maxGas() | uint32 | Gas budget to cap your staticcall at |
isLive() | bool | Hook-level liveness (see caveat below) |
Conventions:
amountSpecifiedfollows v4's convention: negative for exact input, positive for exact output. Exact-input quotes return the expected output; exact-output quotes return the required input.zeroForOne = trueswapscurrency0 -> currency1.hookDatais ignored entirely; pass empty bytes. There are no attestations, signed curves, or per-swap discounts.- The quote already composes the static LP fee (
key.fee) with any v4 protocol fee. Do not re-apply either.
A 0 quote means skip
The views return 0 rather than reverting when the hook cannot price a swap: the pool is paused or un-bootstrapped, effective balances are zero, or an exact-output request exceeds deliverable reserves. Treat 0 as "do not route here right now."
Size Fills Against Effective Liquidity
This is the one accounting subtlety that affects fill sizing:
getReservesis what LPs own:vault.convertToAssets(shares)+ claims + tracked ERC-20getEffectiveLiquidityis what the hook can withdraw and deploy this block:vault.previewRedeem(shares)+ claims + tracked ERC-20
When the pool's ERC-4626 vault is paused, capped, or utilization-constrained, effective liquidity drops below reserves. The JIT cycle and quote path both size against effective liquidity, so plan fills against getEffectiveLiquidity, not getReserves. swapToPrice already does this internally, so prefer it for split planning.
Quote Fidelity
getIndicativeQuote and swapToPrice are a compact, single-step simulation: they sum the liquidity of the distribution buckets in range at the current tick and run one SwapMath.computeSwapStep against that constant liquidity. Implications:
- Accurate for swaps that stay within the current in-range bucket set, the common small and medium swap near the peg
- No tick-walking across bucket boundaries: a large swap that crosses into or out of bucket ranges sees liquidity change mid-swap that the single-step quote does not capture
- The output leg is capped at the effective output reserve, so a quote can never exceed what the pool can deliver
Recommendations:
- Use
getIndicativeQuotefor ranking - Use
swapToPricewith a realsqrtPriceLimitX96for split planning and tighter fills, so the simulation is bounded the same way execution will be - Track persistent quote-vs-fill divergence per pool and feed it into your routing reputation model; the design expects routers to deprioritize on sustained divergence
Failure Modes
| Situation | What you observe | Handling |
|---|---|---|
| Pool paused or not yet bootstrapped | Quote views return 0; direct swap reverts PoolNotLive(poolId) | Skip on 0; pre-filter with livePools(poolId) |
| Vault throttled | getEffectiveLiquidity < getReserves; oversized fills under-fill or revert on the vault withdrawal | Size against effective liquidity |
| Quote/execution divergence on large swaps | Realized output differs from the indicative | Use swapToPrice; track divergence |
isLive() returns true for a paused pool | isLive() is hook-level, not per-pool | Use livePools(poolId) or the 0-quote signal for pool liveness |
Execution
Nothing here is DualPool-specific: swap as you would against any v4 pool, via the PoolManager or the Universal Router, and enforce your own slippage. See Swap Against a DualPool.
Integration Checklist
- Confirm the hook viaÂ
supportsInterface(type(IALFHook).interfaceId) - Quote through
getIndicativeQuote/swapToPrice, never pool liquidity - Pass
hookData =Â "" - Read the fee from
key.fee(static; never the dynamic-fee flag) - Size fills against
getEffectiveLiquidity, notÂgetReserves - Cap quote
staticcallgas atÂmaxGas() - Treat a
0quote as "skip"; pre-filter withlivePools(poolId)for direct routing - Enforce your own slippage on execution
- Track quote fidelity per pool