Contract reference.
Functions, structs, events and errors of the V3 and V4 ladder manager contracts.
Both ladder managers share the same shape of API. The only difference is how the pool is identified: a pool address on V3, a PoolKey on V4.
Open parameters
ILadderManager.OpenParams (V3)
OpenParams { address pool; // canonical Uniswap V3 pool int24 tickLower; // absolute ticks, aligned by the contract int24 tickUpper; uint256 amount0; // max token0 to deposit uint256 amount1; // max token1 to deposit uint8 shape; // 1 Uniform · 2 Bell · 3 Ascending · 4 Descending · 5 Barbell uint8 numBins; // 1 to 20 int24 minTick; // price guard: current tick must be in [minTick, maxTick] int24 maxTick;}ILadderManagerV4.OpenParams (V4)
OpenParams { PoolKey poolKey; // replaces the pool address int24 tickLower; int24 tickUpper; uint256 amount0; // = msg.value when currency0 is native ETH uint256 amount1; uint8 shape; uint8 numBins; int24 minTick; int24 maxTick;}Functions
| Function | Description |
|---|---|
openLadder(OpenParams) → ladderId | Opens a ladder. V4 version is payable. Unused funds are refunded. |
claimFees(ladderId) → (amount0, amount1) | Owner only. Sends fees minus 3%. |
closeLadder(ladderId) → (amount0, amount1) | Owner only. Withdraws everything; 3% applies to fees only. |
getLadder(ladderId) | Owner, pool, bins, range, shape, active flag. |
getLaddersByOwner(owner) | All ladder ids of an address, including closed ones. |
pendingFees(ladderId) | Unclaimed fees, net of the protocol fee. |
positionValue(ladderId) | Token amounts held at the current price, excluding fees. |
inRange(ladderId) | Whether the pool's current tick is inside the ladder range. |
protocolFeeBps() · nextLadderId() | 300, and the id the next ladder will get. |
Events
V3 (V4 names end in V4 and carry a PoolKey instead of the pool)
LadderOpened(address owner, uint256 ladderId, address pool, int24 tickLower, int24 tickUpper, uint256[] positionIds, uint128[] liquidities, uint256 amount0Used, uint256 amount1Used, uint8 shape, uint8 numBins); FeesClaimed(address owner, uint256 ladderId, uint256 amount0, uint256 amount1, uint256 protocolFee0, uint256 protocolFee1); LadderClosed(address owner, uint256 ladderId, uint256 amount0, uint256 amount1, uint256 protocolFee0, uint256 protocolFee1);Errors
| Error | When |
|---|---|
InvalidPool | Not a canonical V3 pool, V4 pool not initialized, or currencies not sorted. |
PriceOutOfBounds(currentTick) | Current tick outside [minTick, maxTick]. |
InvalidRange | tickLower ≥ tickUpper after alignment. |
InvalidShape · InvalidNumBins | Shape not in 1–5, bins not in 1–20. |
InsufficientAmount | Both amounts are zero. |
NoLiquidity | No bin can take the deposited tokens at the current price. |
InvalidValue | V4: msg.value does not match the native amount. |
ExceedsDeposit | V4: the pool asked for more than was deposited. |
NotLadderOwner · LadderNotActive | Claim or close by someone else, or on a closed ladder. |
Example: open a V4 ladder with native ETH
The app computes ticks from the pool's current tick. The same logic, stripped down, with viem (opens in a new tab):
open-ladder-v4.ts
[, tick] = client.readContract({ address: STATE_VIEW, abi: stateViewAbi, functionName: "getSlot0", args: [poolId],}) // Token is currency1: a higher token price is a lower tick.// Range -30% / +40% of the current price: tickLower = Math.floor(tick - Math.log(1.4) / Math.log(1.0001)) tickUpper = Math.ceil(tick - Math.log(0.7) / Math.log(1.0001)) wallet.writeContract({ address: KURVO_LADDER_MANAGER_V4, abi: ladderManagerV4Abi, functionName: "openLadder", args: [{ poolKey, tickLower, tickUpper, amount0: parseEther("0.5"), // ETH (currency0) amount1: 0n, shape: 2, numBins: 10, minTick: tick - 50, maxTick: tick + 50, // ~0.5% price guard }], value: parseEther("0.5"),})