Integrating swaps
Stable Hooks is deployed on Base (pre-launch). Contract addresses are in the Deployments section below and on the Contract Addresses page. This guide is stable and can be built against directly; the deployment addresses may change before the public launch, so confirm before integrating.
This page is for aggregators, solvers, and routers that want to source liquidity from Stable Hooks pools. It covers pool discovery, quoting, execution, indexing, and the ways these pools differ from vanilla v4 pools.
TL;DR: integration is standard Uniswap v4. Build the PoolKey, quote with the v4 Quoter, execute through the Universal Router with empty hookData. The one thing you must not do is price these pools from core pool state: slot0 and tick data are meaningless here (see What not to do).
Pool discovery
Watch the factory for deployments:
event StableSwapHooksDeployed(address indexed _sender, address indexed _hook);For each hook, enumerate its assets and parameters:
uint256 n = hook.currenciesLength(); // 2 to 4
Currency c = hook.currencies(i); // sorted ascending; address(0) = native ETH
uint256 lpFee = hook.lpFeePercentage(); // scaled by 1e6; also the PoolKey.fee value
int24 spacing = hook.TICK_SPACING(); // always 1A hook with
nassets registers alln * (n - 1) / 2pairwise pools at deployment. Every pair is a routable pool; all pairs of one hook draw on the same shared reserves. You can verify a candidate pool id withhook.isValidPoolId(poolId).
The PoolKey for any pair (with currency0 < currency1):
PoolKey memory poolKey = PoolKey({
currency0: currency0,
currency1: currency1,
fee: uint24(hook.lpFeePercentage()),
tickSpacing: hook.TICK_SPACING(),
hooks: IHooks(address(hook))
});Quoting
Via the v4 Quoter (RPC simulation)
The standard v4 Quoter simulates the full swap including the hook and returns correct amounts with no special handling. It is revert-based and not gas-efficient, so call it off-chain over eth_call, not from a contract:
Without RPC: replicating the math
For indicative pricing without RPC simulation, replicate the hook's math from four reads:
Reserves:
hook.reserves(i)for each currency index.Amplification:
hook.getCurrentAmp()(interpolates during ramps).Rates: each reserve is scaled by a per-currency rate before the invariant math. The static rate is
10^(36 - decimals)(native ETH counts as 18 decimals). Ifhook.rateOracles(i)has a non-zero oracle, the effective rate isstaticRate * fetchedRate / 1e18, wherefetchedRateis astaticcallto the configured selector (e.g. wstETH'sstEthPerToken()). SeeBase._getRateandStableSwapMath.scaleToin the repo.Fees: for exact input, compute the raw StableSwap output first, then deduct the gross LP fee from the output:
fee = ceil(rawAmountOut * lpFeePercentage / 1e6),amountOut = rawAmountOut - fee. For exact output, compute the raw input, then add the same gross fee on top:amountIn = rawAmountIn + ceil(rawAmountIn * lpFeePercentage / 1e6). The fee is charged on the raw amount, not grossed up by1 / (1 - fee). The hook/protocol split within the LP fee does not affect trader amounts.
The invariant and target-reserve computation are in src/libraries/StableSwapMath.sol (getInvariant, getTargetReserves); the swap flow that composes them is src/Swap.sol. Cache invalidation: reserves change on every swap and liquidity event (see Indexing); amp changes only during announced ramps; oracle rates drift slowly (staking yield).
Execution
Swaps route through the Universal Router with the ordinary v4 action encoding. No listing, registration, or approval is involved: the router executes against any initialized v4 pool the caller's PoolKey points at, so these pools are callable by anyone from the moment they are deployed.
Notes:
Exact output uses
SWAP_EXACT_OUT_SINGLE, whose params swapamountIn/amountOutMinimumforamountOut/amountInMaximum(sameminHopPriceX36andhookDatafields).Match the params struct to your v4-periphery version.
minHopPriceX36(a per-hop price bound,0to disable) is present in currentIV4Router.ExactInputSingleParams/ExactOutputSingleParamsbut was added after some earlier periphery releases. If your pinned periphery predates it, drop that field; if you are on a newer one, keep it. Always check theIV4Routerstruct in the version you build against.Native ETH: pass value with the call; for exact-output swaps send
amountInMaximumand append aCommands.SWEEPto recover the unused remainder.hookDatais always empty. The hook takes no per-swap parameters.Slippage is enforced by the router's
amountOutMinimum/amountInMaximum, exactly as for any v4 pool.Multi-hop composes normally. These pools participate in standard v4 path swaps (
SWAP_EXACT_IN/SWAP_EXACT_OUT) and can be batched with any other v4 pools in a single Universal Router call. Each hop through a Stable Hooks pool runs its ownbeforeSwap.Universal Router pulls input tokens via Permit2, as for any v4 swap; callers need the usual token-to-Permit2 approval plus a Permit2 allowance for the router.
Custom settlement contracts work too. The Universal Router is the common path, not a requirement. A settlement contract that unlocks the
PoolManagerand callsswap()with thePoolKeydirectly (the pattern aggregator executors typically use) integrates the same way; the hook does not care who the caller is.Dust swaps return zero, they do not revert. The gross LP fee rounds up against the trader, so a tiny exact-input swap (for example 1 wei) executes successfully with
amountOut = 0. Route-splitting logic should floor leg sizes rather than rely on reverts to catch dust legs. Rounding always favors the pool; round-tripping tiny amounts cannot extract value.
Gas costs
Measured end-to-end through the Universal Router against the repo's test suite (execution gas only; add ~21k intrinsic plus calldata):
Exact input, single hop
~176k
~77k
Exact output, single hop
~244k
~88k
The cold column is the representative cost for a standalone swap transaction. The warm column applies when the same pool is touched again within one transaction (multi-hop batches, split routes). These benchmarks are for a plain pool with no oracles. Pools with rate oracles read each asset's rate when building the swap context and again for the swap's input and output assets, so an oracle-configured asset can be read up to twice per swap: a 2-asset pool with both assets oracle-configured does 4 rate staticcalls, a 4-asset pool up to 6. Each costs whatever the token's rate function costs (typically a few thousand gas).
Stability guarantees
Answers to the usual "can this pool change under me" questions:
The trader-facing fee cannot change.
lpFeePercentageis immutable per hook, set at deployment. The hook/protocol split within it is admin-adjustable but never changes trader amounts.Live pools cannot be paused. The factory's
pause()only blocks new pool deployments. There is no admin switch that halts swaps or withdrawals on an existing hook.Amplification changes only gradually and observably. Only the factory owner can ramp A, a ramp takes at least 1 day (
MIN_AMP_RAMP_TIME) and changes A by at most 10x (MAX_AMP_MULTIPLIER), and both ramp start and stop emit events (AmpRampStarted/AmpRampStopped), so quote engines can track A precisely.The asset set is fixed. Currencies and rate-oracle configs are set in the constructor and cannot be added, removed, or repointed.
Revert conditions
Swaps revert when:
Exact output exceeds available reserves. There is no partial fill; size exact-output swaps below
hook.reserves(i)of the output asset (the effective bound is lower once price impact and fees are counted).A configured rate oracle fails.
_getRateperforms astaticcallfor each oracle-configured asset on every swap. If the oracle call itself reverts, that revert bubbles up through OpenZeppelin'sAddress.functionStaticCall(the oracle's own error data, or a failed-call error). The dedicatedRateOracleCallFailederror is raised only when the call succeeds but returns a non-32-byte value. Either way the swap reverts, so do not key retry logic on a single error selector. Plain pools (no oracles) have no such dependency.The pool id is not registered to the hook (
InvalidPoolId). Only the pairwise pools initialized at deployment are valid.Router-level checks fail:
amountOutMinimum/amountInMaximumviolated, expired deadline. Standard v4 behavior.
What not to do
These pools are custom-curve pools. The v4 core pool exists only as a settlement shell:
Do not price from
slot0. Every pairwise pool is initialized atsqrtPriceX96 = 1 << 96(a 1:1 price) and never moves, because the hook consumes 100% ofamountSpecifiedinbeforeSwap(viabeforeSwapReturnDelta) and core swap math runs on zero.Do not read tick data or core liquidity. Native liquidity positions are blocked (
beforeAddLiquidity/beforeRemoveLiquidityrevert), so in-range liquidity is always zero. Depth lives inhook.reserves(i).Do not apply core LP-fee math. The fee in
PoolKey.feeis charged by the hook's own math as described above, not by the core fee mechanism.Do not assume pairwise independence for large flows. All pairs of one hook share reserves, so a large swap on one pair shifts quotes on every other pair of the same hook.
Indexing
Emitted by the hook:
StableSwap fires on every swap with the full fee breakdown, so volume and fee analytics need no core-pool event parsing. Reserve state can always be re-read from hook.reserves(i).
Interfaces and ABI
The contract interface is stable, so integrations can be built now against the public source. The pieces an integrator needs:
Hook (swap, liquidity, fees, amplification, getters):
src/StableSwapHooks.soland its mixinsSwap.sol,Liquidity.sol,Fees.sol,Amp.sol,Base.sol.Factory (deployment,
StableSwapHooksDeployedevent):src/factories/StableSwapHooksFactory.sol.Interfaces:
src/interfaces/.
To generate the JSON ABI, clone the repo and run forge build; artifacts land in out/StableSwapHooks.sol/StableSwapHooks.json. At launch the deployed contracts are verified on the block explorer, which becomes the canonical ABI source alongside the addresses below.
Deployments
Stable Hooks is deployed on Base ahead of its public launch. The addresses below are current pre-launch deployments, published so integrations can be built in parallel; they may change before launch, so confirm before integrating. New pools are deployed permissionlessly through the factory (see Pool discovery); the factory is the source of truth for the current pool set. The canonical registry for all Revert contracts is the Contract Addresses page.
Base (chain ID 8453)
StableSwapHooksFactory
StableSwapZapIn
cbETH / WETH pool (hook + SSLP LP token)
↳ rate oracle: ChainlinkOracleAdapter (cbETH/ETH)
USDC / USDT pool (hook + SSLP LP token)
Each pool address is a StableSwapHooks contract: at once the hook, the AMM, and the ERC-20 LP token (StableSwap LP Token, SSLP). Price these pools through the hook (Quoter / Universal Router), never from core pool slot0 or tick data. For access questions, reach out on Discord.
Security
The contracts were independently audited by PeckShield and went through a public audit competition on Cantina. Source and tests: github.com/revert-finance/stableswap-hooks.
Last updated