This describes a pre-release version of LBAMM. Interfaces and behavior may change.
Verify the exact repository commit before building production integrations.
Adding & Removing Liquidity
This page is the canonical integrator reference for LBAMM liquidity operations:
addLiquidityremoveLiquiditycollectFeescollectTokensOwed(settling “owed” amounts / transfer debt)
It is pool-type-agnostic: all pool types share the same core flow, while poolParams is interpreted by the pool type contract.
Mental model
A liquidity operation is:
- Pool-type computation (how much liquidity changes + what fees are collected)
- Hook validation + hook fees (token0/token1/position/pool)
- Core accounting updates (reserves / fee balances)
- Net settlement (collect from provider or pay to provider)
- (Optional) queued hook-fee transfers executed after the operation
Two important consequences for integrators:
- Hook fees change what the provider pays/receives. You must bound them.
- Outbound transfers can become “owed” instead of reverting. Providers may need to claim later.
Core structs
Hook calldata bundle
Modify liquidity params
struct LiquidityModificationParams {
address liquidityHook;
bytes32 poolId;
// Bounds on the liquidity-change amounts (exclusive of hook fees).
uint256 minLiquidityAmount0;
uint256 minLiquidityAmount1;
uint256 maxLiquidityAmount0;
uint256 maxLiquidityAmount1;
// Bounds on hook fees (token-denominated).
uint256 maxHookFee0;
uint256 maxHookFee1;
// Pool-type-specific bytes.
bytes poolParams;
}
Key semantics (integrator-critical):
minLiquidityAmount*/maxLiquidityAmount*are checked against the raw pool-type amounts (deposit*/withdraw*) exclusive of hook fees.- Hook fees are bounded separately by
maxHookFee0/maxHookFee1. liquidityHookis part of position identity (see below).
Collect fees params
struct LiquidityCollectFeesParams {
address liquidityHook;
bytes32 poolId;
uint256 maxHookFee0;
uint256 maxHookFee1;
bytes poolParams;
}
Liquidity operations include per-hook calldata:
struct LiquidityHooksExtraData {
bytes token0Hook;
bytes token1Hook;
bytes liquidityHook;
bytes poolHook;
}
These fields are opaque to LBAMM core. They are forwarded to the configured hooks (if any).
LiquidityContext (what hooks see)
struct LiquidityContext {
address provider;
address token0;
address token1;
bytes32 positionId;
}
Position identity and liquidityHook binding
LBAMM derives a base position identity that includes:
provider(the caller for these functions)liquidityHook(position hook address)poolId
If you call removeLiquidity / collectFees with a different liquidityHook than the one used when adding liquidity, you will not resolve the same position.
Integrator rule:
Treat
liquidityHookas part of the position key. Persist it and reuse it for future operations.
API surface
addLiquidity
function addLiquidity(
LiquidityModificationParams calldata liquidityParams,
LiquidityHooksExtraData calldata liquidityHooksExtraData
) external payable returns (
uint256 deposit0,
uint256 deposit1,
uint256 fees0,
uint256 fees1
);
Returns:
deposit0,deposit1: amounts deposited into pool reservesfees0,fees1: fee amounts collected from pool fee balances during this operation
removeLiquidity
function removeLiquidity(
LiquidityModificationParams calldata liquidityParams,
LiquidityHooksExtraData calldata liquidityHooksExtraData
) external returns (
uint256 withdraw0,
uint256 withdraw1,
uint256 fees0,
uint256 fees1
);
Returns:
withdraw0,withdraw1: amounts withdrawn from pool reservesfees0,fees1: fee amounts collected from pool fee balances during this operation
collectFees
function collectFees(
LiquidityCollectFeesParams calldata liquidityParams,
LiquidityHooksExtraData calldata liquidityHooksExtraData
) external returns (uint256 fees0, uint256 fees1);
Returns:
fees0,fees1: fee amounts collected from pool fee balances and distributed (or recorded as owed)
Hook participation and hook fees
Liquidity operations may involve up to four hook calls:
- token0 hook
- token1 hook
- position liquidity hook (
liquidityHook) - pool hook
A hook may return hookFee0 and/or hookFee1.
Integrator-impact:
- On addLiquidity, hook fees can increase the net tokens collected from the provider.
- On removeLiquidity and collectFees, hook fees can reduce the net tokens paid out to the provider (or increase what must be returned).
LBAMM enforces fee bounds:
if (hookFee0 > liquidityParams.maxHookFee0 || hookFee1 > liquidityParams.maxHookFee1) {
revert LBAMM__ExcessiveHookFees();
}
Integrator rule:
Always set
maxHookFee0/maxHookFee1to values you are willing to pay. Assume hook fees may be nonzero.
Step-by-step execution semantics
This section describes what LBAMM core does, independent of pool type.
addLiquidity flow (pool-type agnostic)
- Validate pool exists.
- Call pool type:
- returns
(positionId, deposit0, deposit1, fees0, fees1)
- returns
- Enforce amount bounds (exclusive of hook fees):
deposit*must be within[minLiquidityAmount*, maxLiquidityAmount*]
- Execute liquidity hooks (token0/token1/position/pool) and obtain
hookFee0/hookFee1 - Enforce hook fee bounds (
maxHookFee*) - Update pool state:
- reserves increase by
deposit* - fee balances decrease by
fees*
- reserves increase by
- Settle net amounts with provider:
netAmount0 = deposit0 - fees0 + hookFee0
netAmount1 = deposit1 - fees1 + hookFee1
- Emit
LiquidityAdded(poolId, provider, deposit0, deposit1, fees0, fees1) - Execute any queued “hook fees by hook” transfers (if hooks requested them)
removeLiquidity flow (pool-type agnostic)
- Validate pool exists.
- Call pool type:
- returns
(positionId, withdraw0, withdraw1, fees0, fees1)
- returns
- Enforce amount bounds (exclusive of hook fees):
withdraw*must be within[minLiquidityAmount*, maxLiquidityAmount*]
- Execute liquidity hooks and obtain
hookFee0/hookFee1 - Enforce hook fee bounds (
maxHookFee*) - Update pool state:
- reserves decrease by
withdraw* - fee balances decrease by
fees*
- reserves decrease by
- Settle net amounts with provider:
netAmount0 = -(withdraw0 + fees0) + hookFee0
netAmount1 = -(withdraw1 + fees1) + hookFee1
- Emit
LiquidityRemoved(poolId, provider, withdraw0, withdraw1, fees0, fees1) - Execute queued hook-fee transfers (if any)
Note the sign convention: negative net amounts indicate payout to provider.
collectFees flow (pool-type agnostic)
- Validate pool exists.
- Call pool type:
- returns
(positionId, fees0, fees1)
- returns
- Execute liquidity hooks and obtain
hookFee0/hookFee1 - Enforce hook fee bounds (
maxHookFee*) - Update pool state:
- fee balances decrease by
fees*
- fee balances decrease by
- Settle net amounts with provider:
netAmount0 = -(fees0) + hookFee0
netAmount1 = -(fees1) + hookFee1
- Emit
FeesCollected(poolId, provider, fees0, fees1) - Execute queued hook-fee transfers (if any)
Net settlement, native value, and “owed tokens” (debt)
LBAMM settles liquidity operations via an internal helper that uses a clear sign convention:
netAmount > 0→ collect tokens from providernetAmount < 0→ distribute tokens to provider
Wrapped native support (msg.value)
If msg.value > 0 and the relevant token is wrappedNative, the AMM can deposit wrapped native on behalf of the provider.
If native value is sent but not used:
revert LBAMM__ValueNotUsed();
Failed outbound transfers become “owed”
When distributing ERC-20 tokens to the provider (i.e., netAmount < 0 and token is not wrapped native), LBAMM attempts a transfer:
- If the transfer fails, LBAMM stores the amount as tokens owed instead of reverting.
This means integrators must handle a real possibility:
A successful liquidity operation may not immediately pay out all tokens. Some amounts may be claimable later.
Claiming owed tokens
Providers can claim owed balances:
function collectTokensOwed(address[] calldata tokensOwed) external;
Successful claims emit:
event TokensClaimed(address owedTo, address tokenOwed, uint256 amount);
If an owed transfer fails during claim, the claim reverts:
revert LBAMM__TokenOwedTransferFailed();
Events
Core emits:
event LiquidityAdded(bytes32 indexed poolId, address indexed provider, uint256 amount0, uint256 amount1, uint256 fees0, uint256 fees1);
event LiquidityRemoved(bytes32 indexed poolId, address indexed provider, uint256 amount0, uint256 amount1, uint256 fees0, uint256 fees1);
event FeesCollected(bytes32 indexed poolId, address indexed provider, uint256 fees0, uint256 fees1);
Pool types may emit additional pool-specific events.
Integration snippets
These are generic examples. Replace poolParams with pool-type-specific bytes.
Add liquidity (minimal, pool-type-agnostic)
LiquidityModificationParams memory p = LiquidityModificationParams({
liquidityHook: positionHook, // address(0) if none
poolId: poolId,
// Bound the raw deposit amounts returned by the pool type:
minLiquidityAmount0: min0,
minLiquidityAmount1: min1,
maxLiquidityAmount0: max0,
maxLiquidityAmount1: max1,
// Bound hook fees you are willing to pay:
maxHookFee0: maxHookFee0,
maxHookFee1: maxHookFee1,
// Pool-type-defined:
poolParams: poolParams
});
LiquidityHooksExtraData memory hx = LiquidityHooksExtraData({
token0Hook: token0HookData,
token1Hook: token1HookData,
liquidityHook: positionHookData,
poolHook: poolHookData
});
// Approvals (if token is not wrapped native)
IERC20(token0).approve(address(amm), expectedUpperBound0);
IERC20(token1).approve(address(amm), expectedUpperBound1);
// If token0 or token1 is wrappedNative, you may supply msg.value as needed.
(uint256 deposit0, uint256 deposit1, uint256 fees0, uint256 fees1) = amm.addLiquidity{value: msgValue}(p, hx);
Integrator notes:
- Persist
liquidityHookyou used. It is required to resolve the same position later. - If token transfers to the provider fail (not on add, but on remove/collect), the provider may need to call
collectTokensOwed.
Remove liquidity + collect fees
// IMPORTANT: liquidityHook must match the position’s identity.
LiquidityModificationParams memory rp = LiquidityModificationParams({
liquidityHook: positionHook,
poolId: poolId,
// Bound the raw withdraw amounts returned by the pool type:
minLiquidityAmount0: minOut0,
minLiquidityAmount1: minOut1,
maxLiquidityAmount0: maxOut0,
maxLiquidityAmount1: maxOut1,
// Bound hook fees charged during removal:
maxHookFee0: maxHookFee0,
maxHookFee1: maxHookFee1,
poolParams: poolParams
});
(uint256 w0, uint256 w1, uint256 f0, uint256 f1) = amm.removeLiquidity(rp, hx);
// Collect fees (may be separate or periodic)
LiquidityCollectFeesParams memory cp = LiquidityCollectFeesParams({
liquidityHook: positionHook,
poolId: poolId,
maxHookFee0: maxHookFee0,
maxHookFee1: maxHookFee1,
poolParams: poolParams
});
(uint256 cf0, uint256 cf1) = amm.collectFees(cp, hx);
// If any outbound transfers failed and were recorded as owed:
address;
owed[0] = token0;
owed[1] = token1;
amm.collectTokensOwed(owed);
Common failure modes (what can revert and why)
Even before the protocol is live, integrators should assume these classes of failures:
- Pool existence: pool not created / invalid
poolId - Pool type call reverts: invalid
poolParams, pool-type-specific invariants fail - Bounds violated:
- raw
deposit*/withdraw*outside[minLiquidityAmount*, maxLiquidityAmount*] - hook fees exceed
maxHookFee*→LBAMM__ExcessiveHookFees()
- raw
- Hook reverts: token/position/pool hook validation fails
- Native value misuse:
msg.value > 0but wrapped native not used →LBAMM__ValueNotUsed() - State accounting issues: reserve/fee balance underflow/overflow checks; debt storage overflow guards
- Claiming owed tokens: owed transfer fails on claim →
LBAMM__TokenOwedTransferFailed()
Key takeaways
- Liquidity ops are pool-type-defined, but settlement and enforcement are standardized.
- Hook fees can materially change provider economics. Always bound with
maxHookFee0/maxHookFee1. - Some outbound payments may become tokens owed rather than revert; providers may need to call
collectTokensOwed. liquidityHookis part of the position identity — persist and reuse it.
