> For the complete documentation index, see [llms.txt](https://docs.revert.finance/revert/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.revert.finance/revert/technical-docs/stable-hooks/integrating-swaps.md).

# Integrating swaps

{% hint style="info" %}
Stable Hooks is deployed on **Base** (pre-launch). Contract addresses are in the [Deployments](#deployments) section below and on the [Contract Addresses](/revert/resources/contract-addresses.md) page. This guide is stable and can be built against directly; the deployment addresses may change before the public launch, so confirm before integrating.
{% endhint %}

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](#what-not-to-do)).

## Pool discovery

1. Watch the factory for deployments:

```solidity
event StableSwapHooksDeployed(address indexed _sender, address indexed _hook);
```

2. For each hook, enumerate its assets and parameters:

```solidity
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
```

3. 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`):

```solidity
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:

```solidity
(uint256 amountOut,) = quoter.quoteExactInputSingle(
    IV4Quoter.QuoteExactSingleParams({
        poolKey: poolKey,
        zeroForOne: true,
        exactAmount: uint128(amountIn),
        hookData: bytes("")
    })
);
// quoteExactOutputSingle works the same way for exact-output amounts.
```

### 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](#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.

```solidity
bytes memory actions = abi.encodePacked(
    uint8(Actions.SWAP_EXACT_IN_SINGLE), uint8(Actions.SETTLE_ALL), uint8(Actions.TAKE_ALL));

bytes[] memory params = new bytes[](3);
params[0] = abi.encode(IV4Router.ExactInputSingleParams({
    poolKey: poolKey,
    zeroForOne: true,
    amountIn: uint128(amountIn),
    amountOutMinimum: amountOutMin,   // slippage protection
    minHopPriceX36: 0,                // per-hop price bound; 0 disables it (see note)
    hookData: bytes("")
}));
params[1] = abi.encode(poolKey.currency0, amountIn);  // SETTLE_ALL
params[2] = abi.encode(poolKey.currency1, 0);         // TAKE_ALL

bytes memory commands = abi.encodePacked(uint8(Commands.V4_SWAP));
bytes[] memory inputs = new bytes[](1);
inputs[0] = abi.encode(actions, params);
universalRouter.execute(commands, inputs, deadline);
```

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 `staticcall`s, 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:

```solidity
event StableSwap(
    address indexed _sender,
    Currency indexed _currencyIn,
    Currency indexed _currencyOut,
    uint256 _amountIn,
    uint256 _amountOut,
    uint256 _lpFees,
    uint256 _hookFees,
    uint256 _protocolFees
);

event LiquidityAdded(address indexed _sender, uint256[] _amounts, uint256 _shares);
event LiquidityRemoved(address indexed _sender, uint256[] _amounts, uint256 _shares);
event AmpRampStarted(
    address indexed _sender, uint256 _currentAmp, uint256 _nextAmp, uint256 _currentTime, uint256 _nextAmpTime
);
event AmpRampStopped(address indexed _sender, uint256 _currentAmp, uint256 _currentTime);
```

`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.sol`](https://github.com/revert-finance/stableswap-hooks/blob/main/src/StableSwapHooks.sol) and its mixins [`Swap.sol`](https://github.com/revert-finance/stableswap-hooks/blob/main/src/Swap.sol), [`Liquidity.sol`](https://github.com/revert-finance/stableswap-hooks/blob/main/src/Liquidity.sol), [`Fees.sol`](https://github.com/revert-finance/stableswap-hooks/blob/main/src/Fees.sol), [`Amp.sol`](https://github.com/revert-finance/stableswap-hooks/blob/main/src/Amp.sol), [`Base.sol`](https://github.com/revert-finance/stableswap-hooks/blob/main/src/Base.sol).
* **Factory** (deployment, `StableSwapHooksDeployed` event): [`src/factories/StableSwapHooksFactory.sol`](https://github.com/revert-finance/stableswap-hooks/blob/main/src/factories/StableSwapHooksFactory.sol).
* **Interfaces:** [`src/interfaces/`](https://github.com/revert-finance/stableswap-hooks/tree/main/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](#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](/revert/resources/contract-addresses.md) page.

### Base (chain ID 8453)

| Contract                                          | Address                                                                                  |
| ------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| StableSwapHooksFactory                            | [`0x3805…f564`](https://basescan.org/address/0x38059503CD538e375D485357B5DE893080a4f564) |
| StableSwapZapIn                                   | [`0x7d5c…0e73`](https://basescan.org/address/0x7d5cc6095d0FD8534D1e055c17dA794385470e73) |
| cbETH / WETH pool (hook + `SSLP` LP token)        | [`0x90A4…AAA8`](https://basescan.org/address/0x90A40A2A62044e7AF9163E2F36d3AC65eA08AAA8) |
| ↳ rate oracle: ChainlinkOracleAdapter (cbETH/ETH) | [`0xb9Cf…4614`](https://basescan.org/address/0xb9CfC2A6746896cadb34D6CDe407432f8B604614) |
| USDC / USDT pool (hook + `SSLP` LP token)         | [`0x37cA…2aa8`](https://basescan.org/address/0x37cA10C307fA8caA772f3583B3fd1e923DeE2aa8) |

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](https://discord.gg/HXfxKHrRmf).

## 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](https://github.com/revert-finance/stableswap-hooks).
