For the complete documentation index, see llms.txt. This page is also available as Markdown.

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

  1. Watch the factory for deployments:

event StableSwapHooksDeployed(address indexed _sender, address indexed _hook);
  1. 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 1
  1. A hook with n assets registers all n * (n - 1) / 2 pairwise 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 with hook.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:

  1. Reserves: hook.reserves(i) for each currency index.

  2. Amplification: hook.getCurrentAmp() (interpolates during ramps).

  3. 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). If hook.rateOracles(i) has a non-zero oracle, the effective rate is staticRate * fetchedRate / 1e18, where fetchedRate is a staticcall to the configured selector (e.g. wstETH's stEthPerToken()). See Base._getRate and StableSwapMath.scaleTo in the repo.

  4. 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 by 1 / (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 swap amountIn/amountOutMinimum for amountOut/amountInMaximum (same minHopPriceX36 and hookData fields).

  • Match the params struct to your v4-periphery version. minHopPriceX36 (a per-hop price bound, 0 to disable) is present in current IV4Router.ExactInputSingleParams/ExactOutputSingleParams but 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 the IV4Router struct in the version you build against.

  • Native ETH: pass value with the call; for exact-output swaps send amountInMaximum and append a Commands.SWEEP to recover the unused remainder.

  • hookData is 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 own beforeSwap.

  • 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 PoolManager and calls swap() with the PoolKey directly (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):

Swap
First pool touch in tx
Pool state already warm

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. lpFeePercentage is 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. _getRate performs a staticcall for each oracle-configured asset on every swap. If the oracle call itself reverts, that revert bubbles up through OpenZeppelin's Address.functionStaticCall (the oracle's own error data, or a failed-call error). The dedicated RateOracleCallFailed error 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 / amountInMaximum violated, 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 at sqrtPriceX96 = 1 << 96 (a 1:1 price) and never moves, because the hook consumes 100% of amountSpecified in beforeSwap (via beforeSwapReturnDelta) and core swap math runs on zero.

  • Do not read tick data or core liquidity. Native liquidity positions are blocked (beforeAddLiquidity / beforeRemoveLiquidity revert), so in-range liquidity is always zero. Depth lives in hook.reserves(i).

  • Do not apply core LP-fee math. The fee in PoolKey.fee is 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:

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)

Contract
Address

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