StepCurve

StepCurve implements 0 of the fourteen Uniswap v4 callbacks: none.

drag to orbit

Uniswap v4 hook · Curves

StepCurve

A pool whose price moves in discrete steps instead of continuously, so there is no infinitesimal arbitrage to take and a quote holds still long enough to be worth quoting.

Family
Curves
Callbacks
0 of 14
Fee
static
Admin keys
none
Licence
Apache-2.0

How it works

A constant-product pool changes its price on every swap, by any amount, however small. That is elegant and it has a cost that falls entirely on the people providing the liquidity: the price is always slightly wrong by an amount somebody can capture, and the smaller the increment the more often it is worth capturing. Continuous pricing is what makes an AMM permanently arbitrageable rather than occasionally.

Every other market prices in ticks. Equities trade in cents, bonds in thirty-seconds, futures in whatever the exchange decided, and the reason is not tradition: a minimum increment means a quote is worth something for a while, because moving the price at all costs a whole increment rather than a rounding error. This pool prices in increments.

The quote is constant across a band of inventory `stepSize` wide and falls by `stepX96` when the band is crossed, so: price(reserve0) = max(minPriceX96, startPriceX96 - (reserve0 / stepSize) * stepX96) Within a band the pool is a constant-sum market maker at a fixed price, which is to say it fills at exactly the quote with no slippage at all. A swap that would cross bands walks them, filling each at its own price. So a small trade sees a firm quote and no slippage, and a large trade sees exactly the depth the ladder was configured with.

The consequence for arbitrage is the point. An external price move smaller than one increment is not tradeable against this pool at all, because moving the price requires consuming a whole band. Providers are exposed to moves larger than the increment and immune to noise below it, which is the trade every quoting venue in the world makes.

Liquidity is fungible and proportional; see {ForgeCurveHook}. There are no ticks and no ranges, because the ladder is the range.

Prior art

Constant-sum hooks (Uniswap's own constant-sum example, the StableSwap and Orbital submissions) replace the curve with a different continuous one. Bancor's Carbon quotes discrete asymmetric ladders, off v4. Discrete tick pricing is universal outside crypto.

A v4 custom curve that is constant-sum inside a band and steps between bands, so a quote is firm below the increment and depth is exactly what was configured, is the contribution here.

Where it does not help

A swap that crosses many bands walks them one at a time, so gas grows with the number of bands crossed and a swap larger than `MAX_STEPS` bands reverts rather than filling partially. Size `stepSize` against the trades the pool expects. The ladder is also fixed at deployment: a pool whose asset moves far outside the configured range runs out of ladder and stops quoting on that side, which is honest but is not the same as a curve that quotes everywhere.

Using it

Uniswap v4 removed hookData from initialize, so per-pool parameters arrive out of band. Fix them for a pool key whose pool does not exist yet, then initialize. Nobody can change them afterwards, including you.

poolManager.initialize(key, startingSqrtPriceX96);

Parameters

This hook takes no per-pool configuration.

From TypeScript

npm i @hookforge/sdk

import {getHook, hookAddress, poolKeyFor} from "@hookforge/sdk";

const hook = getHook("step-curve");
const key  = poolKeyFor({
  hook: hookAddress("step-curve", 8453),   // Base
  currencyA: USDC, currencyB: WETH,
  tickSpacing: 60,
});

What it reverts with

ErrorMeaning
AlreadyInitialized()Hook was already initialized.
AmountTooSmall()A deposit was too small to mint any shares, or a withdrawal too small to return anything.
ERC20InsufficientAllowance(address,uint256,uint256)Indicates a failure with the spender’s allowance. Used in transfers.
ERC20InsufficientBalance(address,uint256,uint256)Indicates an error related to the current balance of a sender. Used in transfers.
ERC20InvalidApprover(address)Indicates a failure with the approver of a token to be approved. Used in approvals.
ERC20InvalidReceiver(address)Indicates a failure with the token receiver. Used in transfers.
ERC20InvalidSender(address)Indicates a failure with the token sender. Used in transfers.
ERC20InvalidSpender(address)Indicates a failure with the spender to be approved. Used in approvals.
ExpiredPastDeadline()A liquidity modification order was attempted to be executed after the deadline.
InsufficientInitialLiquidity()The first deposit must exceed the permanently locked minimum.
InsufficientReserves()The ladder cannot fill this swap: the pool has run out of the currency being bought.
InvalidLadder()A constructor argument was zero or inconsistent.
InvalidNativePayer(address)The native currency was settled on behalf of a payer other than the contract paying it.
InvalidNativeValue()Native currency was not sent with the correct amount.
LiquidityOnlyViaHook()Liquidity was attempted to be added or removed via the PoolManager instead of the hook.
PoolNotInitialized()Pool was not initialized.
SafeERC20FailedOperation(address)An operation with an ERC-20 token failed.
SwapTooLarge()The swap would cross more than MAX_STEPS bands. Split it, or the pool needs a wider stepSize.
TooMuchSlippage()Principal delta of liquidity modification resulted in too much slippage.

The callbacks it claims

Uniswap v4 reads a hook's permissions from the low fourteen bits of its own address, which is why deploying one means mining a CREATE2 salt. This hook claims 0, so every deployment of it has an address ending in 0x0.

It says what it is, on-chain

Nothing about a hook's address tells an indexer, a wallet, a router or an agent what the pool does, which is why hook discovery today is a curated list. This hook answers for itself, in one eth_call, with no registry in the loop.

cast call $HOOK "hookName()(string)"    # StepCurve
cast call $HOOK "specURI()(string)"     # https://step-curve.pages.dev/hook.json
cast call $HOOK "hookTags()(string[])"  # curve, custom-curve, discrete-pricing, mev, oracle-free

Build, test and deploy

git clone --recurse-submodules https://github.com/nirholas/step-curve
cd step-curve
forge build && forge test

# Dry run: mines the salt, prints the address, sends nothing.
forge script script/Deploy.s.sol --rpc-url $RPC_URL

# For real.
forge script script/Deploy.s.sol --rpc-url $RPC_URL --broadcast --verify

Status

Unaudited. Built to an audited shape, on OpenZeppelin's audited hook bases, and tested against a real PoolManager. No third party has reviewed it. Read "where it does not help" above before putting money behind it. Not affiliated with Uniswap Labs.