Protocol
Contract reference.
Functions, structs, events and errors of ObolLadderManager and ObolLadderManagerV4.
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)
struct 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)
struct 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 7.5%. |
closeLadder(ladderId) → (amount0, amount1) | Owner only. Withdraws everything; 7.5% 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() | 750, and the id the next ladder will get. |
Events
V3 (V4 names end in V4 and carry a PoolKey instead of the pool)
event LadderOpened(address indexed owner, uint256 indexed ladderId, address pool, int24 tickLower, int24 tickUpper, uint256[] positionIds, uint128[] liquidities, uint256 amount0Used, uint256 amount1Used, uint8 shape, uint8 numBins); event FeesClaimed(address indexed owner, uint256 indexed ladderId, uint256 amount0, uint256 amount1, uint256 protocolFee0, uint256 protocolFee1); event LadderClosed(address indexed owner, uint256 indexed 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:
open-ladder-v4.ts
const [, tick] = await 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:const tickLower = Math.floor(tick - Math.log(1.4) / Math.log(1.0001))const tickUpper = Math.ceil(tick - Math.log(0.7) / Math.log(1.0001)) await wallet.writeContract({ address: OBOL_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"),})