This describes a pre-release version of LBAMM. Interfaces and behavior may change.
Verify the exact repository commit before building production integrations.
Dynamic Price Pool
The Dynamic Price Pool is a concentrated-liquidity pool type where liquidity is provided over tick ranges and the execution price moves during swaps as liquidity is consumed across ranges.
This page documents integration-relevant mechanics only:
- Liquidity model (ticks, ranges, active liquidity)
- Pool creation parameters
- Position lifecycle (add → remove → collect fees)
- Fee behavior and how it differs from “owed-fees” collection patterns
- Snap-to-price behavior (
snapSqrtPriceX96) - Swap behavior (input/output paths, price limits, partial fills)
- Events and indexing guidance
This page is written for integrators interacting with LBAMM Core.
Key Integration Concept: AMM-Partitioned State
Dynamic Price Pool is implemented as reusable middleware.
The pool type contract stores pool and position state in storage partitioned by the calling AMM, where:
amm = msg.sender
That means:
- Multiple AMM-core deployments that implement the LBAMM pool-type interface can all use the same Dynamic Price pool-type contract address.
- Pool and position state does not collide across AMMs because it is stored under a per-AMM partition (
globalState[amm]). - The same
poolIdvalue can exist under multiple AMMs (identical parameters → identicalpoolId), but it refers to different pools in different AMM partitions.
Integrator takeaway: a poolId must always be interpreted alongside the AMM address it belongs to.
Liquidity Model
Ticks and ranges
Liquidity is provided over a range:
tickLower(inclusive)tickUpper(exclusive)
Ticks must satisfy:
tickLower < tickUpper- both ticks are within global tick bounds
- both ticks align to the pool’s
tickSpacing
The pool maintains:
sqrtPriceX96(current price as sqrt(token1/token0) * 2^96)tick(the active tick for the current price)liquidity(active liquidity at the current tick)
Price representation
Price is represented as:
sqrtPriceX96 = sqrt(token1 / token0) * 2^96
This aligns with LBAMM’s standardized getCurrentPriceX96 read surface for pool types.
Creating a Dynamic Price Pool
Pools are created via LBAMM core by calling createPool with:
PoolCreationDetails.poolTypeset to the Dynamic Price Pool type address.PoolCreationDetails.poolParamsset to the ABI-encoded bytes ofDynamicPoolCreationDetails.
DynamicPoolCreationDetails
/**
* @dev Parameters specific to a dynamic pool creation.
* @param tickSpacing Tick spacing.
* @param sqrtPriceRatioX96 Initial price ratio as sqrt(price) * 2^96
*/
struct DynamicPoolCreationDetails {
int24 tickSpacing;
uint160 sqrtPriceRatioX96;
}
Field semantics
-
tickSpacing- Granularity of valid ticks.
- All positions must use ticks aligned to this spacing.
- Also included in the
poolIdderivation.
-
sqrtPriceRatioX96- Initial pool price at creation.
- Must be within supported bounds (see constants below).
- Used to initialize the pool state:
sqrtPriceX96tick = TickMath.getTickAtSqrtPrice(sqrtPriceRatioX96)liquidity = 0
Bounds and constants
/// @dev Minimum tick value for concentrated liquidity positions, equivalent to price of ~1e-38
int24 constant MIN_TICK = -887_272;
/// @dev Maximum tick value for concentrated liquidity positions, equivalent to price of ~1e38
int24 constant MAX_TICK = 887_272;
/// @dev Tick spacing must be at least 1.
int24 constant MIN_TICK_SPACING = 1;
/// @dev Tick spacing is capped at 16384 to prevent overflow in tick traversal helpers.
int24 constant MAX_TICK_SPACING = 16_384;
/// @dev Equivalent to getSqrtPriceAtTick(MIN_TICK)
uint160 constant MIN_SQRT_RATIO = 4_295_128_739;
/// @dev Equivalent to getSqrtPriceAtTick(MAX_TICK)
uint160 constant MAX_SQRT_RATIO = 1_461_446_703_485_210_103_287_273_052_203_988_822_378_723_970_342;
Pool identity
The poolId is deterministic and includes:
- pool type address
- LP fee (BPS)
- tick spacing
- token0, token1
- pool hook address
This means you can deploy multiple pools for the same token pair that differ by fee, tick spacing, or hook.
Example: createPool
DynamicPoolCreationDetails memory dynamicDetails = DynamicPoolCreationDetails({
tickSpacing: 60,
sqrtPriceRatioX96: initialSqrtPriceX96
});
PoolCreationDetails memory details = PoolCreationDetails({
poolType: DYNAMIC_POOL_TYPE,
fee: 30, // 0.30%
token0: token0,
token1: token1,
poolHook: address(0),
poolParams: abi.encode(dynamicDetails)
});
(bytes32 poolId,,) = lbamm.createPool(
details,
"",
"",
"",
""
);
Positions and Position IDs
LBAMM core derives a stable ammBasePositionId from:
provider(the liquidity provider)liquidityHookpoolId
Dynamic Price Pool derives the final positionId by combining:
ammBasePositionIdtickLowertickUpper
Conceptually:
- same provider + same hook + same pool + same range ⇒ same position
Liquidity hooks are common in this pool type. Using different liquidityHook addresses results in distinct base IDs and therefore distinct positions, even for the same provider and tick range.
Liquidity Operations
Dynamic Price Pool supports:
addLiquidityremoveLiquiditycollectFees
Unlike many router patterns, liquidity modification is specified directly in liquidity units.
DynamicLiquidityModificationParams
Used for addLiquidity and removeLiquidity.
/**
* @dev Parameters for modifying dynamic pool liquidity.
*
* @param tickLower Lower bound of the liquidity range
* @param tickUpper Upper bound of the liquidity range
* @param liquidityChange Amount of liquidity to add (positive) or remove (negative)
* @param snapSqrtPriceX96 Price to move to prior to adding liquidity (optional)
*/
struct DynamicLiquidityModificationParams {
int24 tickLower;
int24 tickUpper;
int128 liquidityChange;
uint160 snapSqrtPriceX96;
}
Field semantics
-
tickLower,tickUpper- Define the position range.
- Must be ordered, within bounds, and aligned to tick spacing.
-
liquidityChange- The net change in liquidity, not token amounts.
- For
addLiquidity, must be >= 0. - For
removeLiquidity, must be <= 0. 0is allowed and acts like an expensive “collect-on-modify” operation (it still runs the modification path and fee realization rules).
-
snapSqrtPriceX96- Optional.
- Only applied during
addLiquidity. - If non-zero, attempts to move the pool price to the snap price before adding liquidity, subject to strict constraints (see Section 6).
What amounts mean in LBAMM
For liquidity modification operations, the pool type returns:
- token amounts required to deposit (for add)
- token amounts to withdraw (for remove)
- fees realized during the operation
LBAMM core performs the token movements and enforces user-supplied bounds (min/max fields in core liquidity params).
Example: addLiquidity
DynamicLiquidityModificationParams memory dynamicParams = DynamicLiquidityModificationParams({
tickLower: -600,
tickUpper: 600,
liquidityChange: int128(uint128(liquidityToAdd)),
snapSqrtPriceX96: 0
});
LiquidityModificationParams memory params = LiquidityModificationParams({
liquidityHook: liquidityHook,
poolId: poolId,
minLiquidityAmount0: 100e18,
minLiquidityAmount1: 50e18,
maxLiquidityAmount1: 110e18,
maxLiquidityAmount1: 60e18,
maxHookFee0: 1e17,
maxHookFee1: 0,
poolParams: abi.encode(dynamicParams)
});
lbamm.addLiquidity(params, hooksExtraData);
Example: removeLiquidity
DynamicLiquidityModificationParams memory p = DynamicLiquidityModificationParams({
tickLower: -600,
tickUpper: 600,
liquidityChange: -int128(uint128(liquidityToRemove)),
snapSqrtPriceX96: 0 // ignored for remove
});
LiquidityModificationParams memory params = LiquidityModificationParams({
liquidityHook: liquidityHook,
poolId: poolId,
minLiquidityAmount0: 100e18,
minLiquidityAmount1: 50e18,
maxLiquidityAmount1: 110e18,
maxLiquidityAmount1: 60e18,
maxHookFee0: 1e17,
maxHookFee1: 0,
poolParams: abi.encode(dynamicParams)
});
amm.removeLiquidity(params, hooksExtraData);
Fee Collection and Fee Realization
Dynamic Price Pool supports fee collection in two ways:
- Explicit fee collection via
collectFees - Automatic fee realization during
addLiquidity/removeLiquidity
A key integration difference from “owed-fees then collect later” patterns:
- Fees are not persisted into a long-lived “tokens owed” storage field that must be collected separately.
- Fees are realized and returned as part of these operations, and LBAMM core settles them to the provider in the same call.
DynamicLiquidityCollectFeesParams
/**
* @dev Parameters for collecting dynamic pool fees.
* @param tickLower Lower bound of the liquidity range
* @param tickUpper Upper bound of the liquidity range
*/
struct DynamicLiquidityCollectFeesParams {
int24 tickLower;
int24 tickUpper;
}
Example: collectFees (no liquidity modification)
DynamicLiquidityCollectFeesParams memory dynamicParams = DynamicLiquidityCollectFeesParams({
tickLower: -600,
tickUpper: 600
});
LiquidityCollectFeesParams memory params = LiquidityCollectFeesParams({
liquidityHook: liquidityHook,
poolId: poolId,
maxHookFee0: 1e17,
maxHookFee1: 0,
poolParams: abi.encode(dynamicParams)
});
lbamm.collectFees(params, hooksExtraData);
Snap-to-Price Behavior (snapSqrtPriceX96)
When adding liquidity, you may optionally provide a snapSqrtPriceX96 to reposition the pool price within an empty region (a region with zero liquidity).
This is intended for cases where the current price is in a gap with no active liquidity and you want to mint liquidity at a specific target price within that same gap.
Constraints
Snapping is only allowed if moving from the current price to the target price:
- does not cross any tick with liquidity, and
- does not encounter a tick boundary that would require liquidity transition behavior.
Practical mental model:
- Snapping can move the pool price within a continuous “zero-liquidity corridor.”
- If the move would require crossing into or through liquidity, snapping reverts.
State updates on snap
If snap succeeds, the pool updates only:
sqrtPriceX96 = snapSqrtPriceX96tick = TickMath.getTickAtSqrtPrice(snapSqrtPriceX96)
There are:
- no oracle/observation updates,
- no fee growth updates,
- no liquidity updates,
because the snap is only permitted when no liquidity transitions can occur.
Swaps
Dynamic Price Pool supports both optimized swap paths:
- swap by input amount
- swap by output amount
Swaps update, in the usual concentrated-liquidity manner:
sqrtPriceX96tick- active
liquidity - fee growth accumulators
Price limits via swapExtraData
Swaps may include a price limit encoded in swapExtraData.
For Dynamic Price Pool:
swapExtraDatamust be a 32-byte ABI-encodeduint160:sqrtPriceLimitX96
uint160 sqrtPriceLimitX96 = /* ... */;
bytes memory swapExtraData = abi.encode(sqrtPriceLimitX96);
If no price limit is desired, integrations may supply an empty bytes value for swapExtraData which will set the limit at the minimum or maximum price depending on swap direction.
Partial fills
Dynamic Price Pool supports partial fills when the swap reaches:
- the pool’s min/max price boundary, or
- a swapper-supplied
sqrtPriceLimitX96
In these cases, the pool returns:
- the actual amount consumed/produced up to the boundary, and
- the swap completes without reverting solely due to hitting the limit.
Read Surface: getCurrentPriceX96
Dynamic Price Pool implements the standardized price read surface:
function getCurrentPriceX96(address amm, bytes32 poolId)
external
view
returns (uint160 sqrtPriceX96);
Because state is AMM-partitioned, callers must pass the correct amm (the AMM core address whose pool state you want to query).
Events and Indexing
Dynamic Price Pool emits pool-type-specific events intended for indexers to reconstruct pool and position state.
/// @dev Event emitted when a swap occurs in a dynamic pool, containing pool-specific details
event DynamicPoolSwapDetails(
address indexed amm,
bytes32 indexed poolId,
uint160 sqrtPriceX96,
uint128 liquidity,
int24 tick
);
/// @dev Event emitted when liquidity is added to a dynamic pool position
event DynamicPoolLiquidityAdded(
address indexed amm,
bytes32 indexed poolId,
bytes32 indexed positionId,
int128 liquidity,
int24 tickLower,
int24 tickUpper
);
/// @dev Event emitted when liquidity is removed from a dynamic pool position
event DynamicPoolLiquidityRemoved(
address indexed amm,
bytes32 indexed poolId,
bytes32 indexed positionId,
int128 liquidity,
int24 tickLower,
int24 tickUpper
);
Indexing guidance (event-driven reconstruction)
Indexers should treat (amm, poolId) as the pool key.
A practical reconstruction strategy:
-
Track pool state from swaps
- On
DynamicPoolSwapDetails(amm, poolId, sqrtPriceX96, liquidity, tick):- set current
sqrtPriceX96, currenttick, and current activeliquidity.
- set current
- On
-
Track position definitions from liquidity events
- On
DynamicPoolLiquidityAdded/Removed:- map
positionId -> (amm, poolId, tickLower, tickUpper) - accumulate net liquidity changes per position.
- map
- On
-
Track fee attribution externally
- Fee growth state is internal to the pool type; the canonical offchain path is:
- maintain the pool state from swap events, and
- maintain positions from add/remove events,
- then compute position fee attribution according to the concentrated-liquidity fee growth model.
- Fee growth state is internal to the pool type; the canonical offchain path is:
The pool also returns realized fees during liquidity operations and explicit fee collection through LBAMM core; those flows can be indexed from the AMM core events associated with those operations.
Summary
Dynamic Price Pool provides a concentrated-liquidity market design with:
- tick-range positions,
- moving price during swaps,
- liquidity changes specified directly as
liquidityChange(not token amounts), - fee realization returned during liquidity operations (no persisted “owed-fees” storage),
- efficient fee-only collection via
collectFees, - optional snap-to-price during
addLiquiditywithin zero-liquidity regions, and - partial fills when swaps hit min/max boundaries or a user-supplied
sqrtPriceLimitX96.
The most important integration boundary is that pool state is partitioned by AMM: always interpret poolId in the context of its AMM address.
