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

Deploying a pool

Stable Hooks pools are permissionless. Anyone can deploy one by calling the factory on-chain: there is no allowlist and no approval step. StableSwapHooksFactory.deploy(...) is whenNotPaused with no owner gate, so any address can call it while the factory is unpaused.

There is no UI for this today, so deploying a pool is a contract-level operation aimed at developers and issuers. This page documents the full path. The factory address is on the Contract Addresses page.

The reference implementation of everything below is script/DeployStableSwapPool.s.sol (a Foundry script) and script/hookMiner.demo.js (a standalone salt miner) in github.com/revert-finance/stableswap-hooks.

Chains

The factory is deployed on Base only (address on the Contract Addresses page). Deploying a pool requires the factory, so pools can currently be created on Base only. To bring Stable Hooks to another chain, reach out on Discord; we deploy and configure the factory per chain.

What you control, and what you don't

You set these at deploy time, and they are immutable for the life of the pool:

  • the currency set (2 to 4 correlated assets),

  • the LP fee charged to traders,

  • the amplification coefficient (A),

  • the per-currency rate oracles.

Two things are administered by the factory owner, not the deployer:

  • The hook/protocol fee split (setProtocolFeePercentage, setHookFeePercentage) is onlyFactoryOwner. On a self-deployed pool these default to 0, so the entire net LP fee accrues to LPs. The trader-facing LP fee you set at deploy is immutable and is never changed by this split. This mirrors the behavior described under Integrating swaps → Stability guarantees.

  • Amplification ramping (startAmpRamp, stopAmpRamp) is onlyFactoryOwner. Your initial baseAmp is fixed at deploy; only the factory owner can later ramp it, and only gradually (see limits below).

Parameters

Parameter
Type
Rules

currencies

Currency[]

2 to 4 entries (MIN_CURRENCIES..MAX_CURRENCIES), sorted strictly ascending by address. Native ETH is address(0).

rateOracles

RateOracleConfig[]

One per currency (length must match). {oracle: address(0), selector: 0x00000000} for a static 1:1 asset. For a yield-bearing asset, point at its rate function (e.g. wstETH stEthPerToken()) so it is priced at its true ratio.

lpFeePercentage

uint256

Trader-facing LP fee, scaled by FEE_PRECISION = 1e6. Must be 0 < fee < 1e6. Example: 500 = 0.05%. Stored in PoolKey.fee.

baseAmp

uint256

Initial amplification, unscaled. Must be < MAX_AMP (1,000,000); use a non-zero value. Higher A prices tighter around the reference ratio.

Deploy flow

1. Verify the creation bytecode

The factory validates that the creation code you pass hashes to a stored value, so you must use the exact StableSwapHooks creation bytecode:

2. Build the constructor args

The factory appends abi.encode(poolManager, currencies, rateOracles, lpFeePercentage, baseAmp) to the creation code. Use the factory's own poolManager():

3. Mine a CREATE2 salt

A Uniswap v4 hook address must encode its permission flags in its low 14 bits. Stable Hooks uses factory.HOOK_FLAGS() (0x2AA8 = 10920: BEFORE_INITIALIZE, BEFORE_ADD_LIQUIDITY, BEFORE_REMOVE_LIQUIDITY, BEFORE_SWAP, BEFORE_SWAP_RETURNS_DELTA, BEFORE_DONATE). Mine a salt whose predicted CREATE2 address matches. This is pure off-chain computation and costs no gas.

script/hookMiner.demo.js does the same in standalone JavaScript (mask 0x3fff, deployer = the factory address) if you are not using Foundry.

4. Deploy

This deploys the hook via CREATE2 and initializes every pairwise pool for the currency set (n * (n - 1) / 2 pools), all sharing one set of reserves. It emits StableSwapHooksDeployed(msg.sender, hook), which integrators watch for discovery (see Integrating swaps → Pool discovery).

5. Seed initial liquidity

The pool is live but empty. Add the first liquidity so swaps can execute. Approve the tokens to the hook, preview with quoteAddLiquidity, then deposit:

The first deposit permanently locks MINIMUM_LIQUIDITY (1000) shares to a dead address to prevent share-price manipulation. Net LP fees stay in reserves, so fees compound into every LP share automatically; there is no claim step. Withdraw with removeLiquidity(shares, minAmounts).

Foundry one-liner

The reference script mines the salt, deploys, and (as factory owner) configures the fee split:

Edit the currency addresses, LP_FEE_PERCENTAGE, and BASE_AMP constants in the script for your pool. The setProtocolFeePercentage/setHookFeePercentage calls at the end only succeed if the sender is the factory owner; drop them if you are deploying a third-party pool (the split then stays at 0).

Common reverts

Revert
Cause

CurrenciesNotSorted

currencies not strictly ascending by address

InvalidCurrenciesLength

fewer than 2 or more than 4 currencies

InvalidFeePercentage

lpFeePercentage is 0 or >= 1e6

InvalidRateOraclesLength

rateOracles.length != currencies.length

InvalidCreationCode

keccak256(creationCode) != factory.creationCodeHash()

InvalidAmp

baseAmp >= MAX_AMP (1,000,000)

EnforcedPause

the factory is paused

hook address mismatch

the mined salt does not encode HOOK_FLAGS; re-mine against the exact constructorArgs

Amplification ramping (factory owner)

baseAmp is fixed at deploy. Changing it later is a factory-owner action and is rate-limited for pool safety: at most a 10x change (MAX_AMP_MULTIPLIER) per ramp, at least MIN_AMP_RAMP_TIME (1 day) between ramps, and the ramp end must be at least 1 day out. getCurrentAmp() returns the interpolated value during a ramp.

Last updated