Add/Remove Liquidity
Introduction
Liquidity management is performed through LBRouter
contract. This contract will abstract some of the complexity of the liquidity management, perform safety checks and will revert if certain conditions were to not be met.
- Liquidity is added or removed to
LBPairs
. - Liquidity may be distributed to specific
Bins
, with different amounts perBin
.
- The v2.2
LBRouter
is not backwards compatible with v2.1LBPairs
, although ABI stays the same - The v2.1
LBRouter
is not backwards compatible with v2.0LBPairs
. - The v2.0
LBRouter
must be used to remove liquidity from v2.0LBPairs
and v2.1LBRouter
must be used to remove liquidity from v2.1LBPairs
Adding Liquidity
To add liquidity, the LiquidityParameters
struct is as input:
function addLiquidity(LiquidityParameters memory liquidityParameters)
external
returns (
uint256 amountXAdded,
uint256 amountYAdded,
uint256 amountXLeft,
uint256 amountYLeft,
uint256[] memory depositIds,
uint256[] memory liquidityMinted
);
function addLiquidityNATIVE(LiquidityParameters memory liquidityParameters)
external
payable
returns (
uint256 amountXAdded,
uint256 amountYAdded,
uint256 amountXLeft,
uint256 amountYLeft,
uint256[] memory depositIds,
uint256[] memory liquidityMinted
);
Liquidity Parameters
struct LiquidityParameters {
IERC20 tokenX; // Has to be the same as tokenX defined in LBPair contract
IERC20 tokenY; // Has to be the same as tokenY defined in LBPair contract
uint256 binStep; // Has to point to existing pair
uint256 amountX; // Amount of token X that you want to add to liquidity
uint256 amountY; // Amount of token Y that you want to add to liquidity
uint256 amountXMin; // Defines amount slippage for token X
uint256 amountYMin; // Defines amount slippage for token Y
uint256 activeIdDesired; // The active bin you want. It may change due to slippage
uint256 idSlippage; // The slippage tolerance in case active bin moves during time it takes to transact
int256[] deltaIds; // The bins you want to add liquidity to. Each value is relative to the active bin ID
uint256[] distributionX; // The percentage of X you want to add to each bin in deltaIds
uint256[] distributionY; // The percentage of Y you want to add to each bin in deltaIds
address to; // Receiver address
address refundTo; // Refund Address
uint256 deadline; // Block timestamp cannot be lower than deadline
}
The number of parameters are quite extensive. Here are a few pointers to understand how to construct them better:
- The active bin ID may change from the time you decided to add liquidity to when it is actually added. Therefore, you define
activeIdDesired
andidSlippage
to account for when the price moves. deltaIds
define which bins liquidity will be added to relative toactiveId
, 0 being the active bin. All positive values are bins with only X and all negative values are bins with only Y.distributionX
(ordistributionY
) is the percentages ofamountX
(oramountY
) you want to add to each bin.- Sum of all values should be less than or equal to 1. If less than, the remaining is refunded back to the user.
- Trying to add X to a bin below the active bin or Y to a bin above the active bin will cause a revert.
- Maximum number of bins, that can be populated at the same time is around
80
on Avalanche C-chain due to block gas limit (8M
). Multiple transactions can be used to add liquidity to more bins.
Code Example
In this example, we add 100 USDC and 100 USDT into three bins: active bin, bin below and bin above.
We define the distributions as follow:
- For asset X (USDC), we add 50 USDC to the active bin and 50 USDC to the bin above.
- For asset Y (USDT), we add 33.3 USDT to the active bin and 66.6 USDT to the bin below.
We also allow a bin ID slippage of 5 just in case bin moves in the time it takes to execute the transaction.
uint256 PRECISION = 1e18;
uint256 binStep = 25;
uint256 amountX = 100 * 10e6;
uint256 amountY = 100 * 10e6;
uint256 amountXmin = 99 * 10e6; // We allow 1% amount slippage
uint256 amountYmin = 99 * 10e6; // We allow 1% amount slippage
uint256 activeIdDesired = 2**23; // We get the ID from price using getIdFromPrice()
uint256 idSlippage = 5;
uint256 binsAmount = 3;
int256[] memory deltaIds = new int256[](binsAmount);
deltaIds[0] = -1;
deltaIds[1] = 0;
deltaIds[2] = 1;
uint256[] memory distributionX = new uint256[](binsAmount);
distributionX[0] = 0;
distributionX[1] = PRECISION / 2;
distributionX[2] = PRECISION / 2;
uint256[] memory distributionY = new uint256[](binsAmount);
distributionY[0] = (2 * PRECISION) / 3;
distributionY[1] = PRECISION / 3;
distributionY[2] = 0;
ILBRouter.LiquidityParameters memory liquidityParameters = ILBRouter.LiquidityParameters(
USDC,
USDT,
binStep,
amountX,
amountY,
amountXmin,
amountYmin,
activeIdDesired,
idSlippage,
deltaIds,
distributionX,
distributionY,
receiverAddress,
refundAddress,
block.timestamp
);
USDC.approve(address(router), amountX);
USDT.approve(address(router), amountY);
(
uint256 amountXAdded,
uint256 amountYAdded,
uint256 amountXLeft,
uint256 amountYLeft,
uint256[] memory depositIds,
uint256[] memory liquidityMinted
) = router.addLiquidity(liquidityParameters);
Removing Liquidity
There are some key differences between adding and removing liquidity:
- We don't use the
LiquidityParameters
struct. - We use absolute bin IDs instead of relative bin IDs.
- Because we use absolute bin IDs, bin slippage is not possible.
- We define absolute
LBToken
balances to remove from each bin.- In bins below active bin, balances consist of only Y.
- In bins above active bin, balances consist of only X.
- In the active bin, the balance consists of a share of X and Y.
To remove liquidity, we use one of the router functions below:
function removeLiquidity(
IERC20 tokenX,
IERC20 tokenY,
uint16 binStep, // Has to point to existing pair that user has liquidity deposited in
uint256 amountXMin, // Minimum amount of token X that has to be withdrawn
uint256 amountYMin, // Minimum amount of token Y that has to be withdrawn
uint256[] memory ids, // Bin IDs that liquidity should be removed from
uint256[] memory amounts, // LBToken amount that should be removed
address to, // Receiver address
uint256 deadline // Block timestamp cannot be lower than deadline
) external returns (uint256 amountX, uint256 amountY);
function removeLiquidityNATIVE(
IERC20 token,
uint16 binStep,
uint256 amountTokenMin,
uint256 amountNATIVEMin,
uint256[] memory ids,
uint256[] memory amounts,
address payable to,
uint256 deadline
) external returns (uint256 amountToken, uint256 amountNATIVE);
Here are some pointer for using these functions:
- Lengths of
ids
andamounts
must be the same. - Values in
amounts
areLBToken
amounts. - Maximum number of bins that can be withdrawn at the same time is around
51
due to Avalanche C-chain block gas limit (8M
). In this case, multiple transactions can be used to remove more liquidity.
For tax tokens, removing liquidity with removeLiquidityNATIVE()
is not possible, due to double tax accrual.
This can be circumvented in two ways, depending on tax token implementation:
- Whitelisting LBRouter and/or LBPair.
- Removing native liquidity with
removeLiquidity()
function. This will return wrapped native token to user, instead of just native token.
Code Example
uint256 numberOfBinsToWithdraw = 3;
uint16 binStep = 25;
uint256[] memory amounts = new uint256[](numberOfBinsToWithdraw);
uint256[] memory ids = new uint256[](numberOfBinsToWithdraw);
ids[0] = 8388608;
ids[1] = 8388611;
ids[2] = 8388605;
uint256 totalXBalanceWithdrawn;
uint256 totalYBalanceWithdrawn;
// To figure out amountXMin and amountYMin, we calculate how much X and Y underlying we have as liquidity
for (uint256 i; i < numberOfBinsToWithdraw; i++) {
uint256 LBTokenAmount = pair.balanceOf(receiverAddress, ids[i]);
amounts[i] = LBTokenAmount;
(uint256 binReserveX, uint256 binReserveY) = pair.getBin(uint24(ids[i]));
totalXBalanceWithdrawn += LBTokenAmount * binReserveX / pair.totalSupply(ids[i]);
totalYBalanceWithdrawn += LBTokenAmount * binReserveY / pair.totalSupply(ids[i]);
}
uint256 amountXMin = totalXBalanceWithdrawn * 99 / 100; // Allow 1% slippage
uint256 amountYMin = totalYBalanceWithdrawn * 99 / 100; // Allow 1% slippage
pair.approveForAll(address(router), true);
router.removeLiquidity(
USDC,
WAVAX,
binStep,
amountXMin,
amountYMin,
ids,
amounts,
receiverAddress,
block.timestamp
);