Skip to content

Remove Liquidity


Prereq: Add Liquidity


This page covers the necessary concepts to understand the remove liquidity flow in Uniswap V2.

What does burn do:

  • Caches current k (used to calculate fees)
  • Calculates Protocol Fees (optional)
  • Calculates Trading Fees
  • Transfers to user:
    • proportional slice of the pool's current reserves + trading fees - protocol fees
  • Burns user’s LP

Removing liquidity tokens (ERC20) releases a proportional slice of the pool’s current reserves to the provider.

This is where LPs realize “Impermanent Loss” if the relative price of the assets has diverged from when they entered.

Let’s analyze the formula that calculates the returned amounts:

// contracts/uniswap-v2/v2-core/contracts/UniswapV2Pair.sol:138:145
uint balance0 = IERC20(_token0).balanceOf(address(this));
uint balance1 = IERC20(_token1).balanceOf(address(this));
uint liquidity = balanceOf[address(this)];
bool feeOn = _mintFee(_reserve0, _reserve1);
uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution

It calculates the pro-rata distribution (no dilution) using:

xout=liquidityburnedliquiditytotalxreserveyout=liquidityburnedliquiditytotalyreserve\begin{aligned} x_{out} = \frac{ liquidity_{burned} } { liquidity_{total} } \cdot x_{reserve} \\[10pt] y_{out} = \frac{ liquidity_{burned} } { liquidity_{total} } \cdot y_{reserve} \end{aligned}

The calculation uses actual token balances (not just reserves), which include accumulated fees

The protocol fee is a mechanism designed to redirect a portion of the trading fees from liquidity providers to a designated beneficiary.

Uniswap V2 collects accumulated fees only during liquidity events (mint or burn) for efficiency.

The contract tracks the growth of the pool’s invariant between these events to determine the protocol’s share of the earnings.

The protocol fee is minted before burning liquidity via _mintFee(), to capture fees before liquidity removal.

The total collected fees are computed by measuring the growth in k (that is, x · y) since the last time fees were collected.

  • Once feeTo is defined,
    • The protocol begins charging a 5-basis-point (0.05%) fee
    • This is effectively a 1/6th cut of the standard 30-basis-point (0.30%) trading fee, leaving LPs with 25 basis points (0.25%) per trade

Let:

  • s1s_1 = the total number of LP tokens that already exist at the fee-accounting checkpoint.
  • sms_m = the number of additional LP tokens created specifically for the protocol fee recipient during _mintFee. These are new ownership units that did not exist before and are added on top of the existing supply
  • L1=k1L_1=\sqrt{k_1} = liquidity measured at the previous fee-accounting checkpoint.
  • L2=k2L_2=\sqrt{k_2} = liquidity measured now, when the contract is deciding whether protocol fee LP tokens should be minted.

Therefore:

  • L2L1L_2 −L_1 = growth in liquidity
  • s1+sms_1 +s_m = total LP supply

The protocol should get (1/6) of that growth, so after minting its share of the pool must satisfy:

sms1+sm=16L2L1L2sm=k2k15k2+k1s1\begin{aligned} \frac{s_m}{s_1+s_m}=\frac{1}{6}\cdot\frac{L_2-L_1}{L_2} \\[10pt] s_m=\frac{\sqrt{k_2}-\sqrt{k_1}}{5\sqrt{k_2}+\sqrt{k_1}}\cdot s_1 \end{aligned}

Called at the start of liquidity add/remove accounting, using the old reserves from getReserves(), before any new reserve update happens.

// contracts/uniswap-v2/v2-core/contracts/UniswapV2Pair.sol:89:107
function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
address feeTo = IUniswapV2Factory(factory).feeTo();
feeOn = feeTo != address(0);
uint _kLast = kLast; // gas savings
if (feeOn) {
if (_kLast != 0) {
uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
uint rootKLast = Math.sqrt(_kLast);
if (rootK > rootKLast) {
uint numerator = totalSupply.mul(rootK.sub(rootKLast));
uint denominator = rootK.mul(5).add(rootKLast);
uint liquidity = numerator / denominator;
if (liquidity > 0) _mint(feeTo, liquidity);
}
}
} else if (_kLast != 0) {
kLast = 0;
}
}
  • The Beneficiary (feeTo): the address collecting protocol fee
  • The Controller (feeToSetter): address (controlled by a private key or governance) that has the exclusive right to update the feeTo address and change the feeToSetter itself

You can verify that the burn formula uses actual token balances (not just reserves), which include accumulated fees.

Loading graph...

In this example we will remove liquidity from a pool.

This is the code that runs to produce the function trace:

// UniswapV2Workflows:removeLiquidity:138:171
async removeLiquidity({
user = USER_1.address,
liquidityToRemove = 100n * _1e18,
amountAMin = 50n * _1e18,
amountBMin = 50n * _1e18,
} = {}) {
const { erc20TokenA, erc20TokenB } = await this.initialLiquidity({
user: USER_1.address,
amountADesired: 200n * _1e18,
amountBDesired: 400n * _1e18,
traceTx: false,
});
const pairAddress = await this.calculatePairAddress(erc20TokenA, erc20TokenB);
this.lensClient.addressLabeler.markContractAddress(
pairAddress,
'contracts/uniswap-v2/v2-core/contracts/UniswapV2Pair.sol:UniswapV2Pair'
);
const path: Address[] = [erc20TokenA.address, erc20TokenB.address];
await this._swap(erc20TokenA, 50n * _1e18, 50n * _1e18, path, user, this.maxUint256(), user, false);
const trace = await this._removeLiquidity(
erc20TokenA,
erc20TokenB,
liquidityToRemove,
amountAMin,
amountBMin,
user,
this.maxUint256()
);
return { trace, erc20TokenA, erc20TokenB };
}
// UniswapV2Workflows:_removeLiquidity:227:255
private async _removeLiquidity(
tokenA: UniswapV2ERC20,
tokenB: UniswapV2ERC20,
liquidity: bigint,
amountAMin: bigint,
amountBMin: bigint,
user: Address,
deadline: bigint,
trace = true
) {
const { router2 } = this.deployment;
const pairAddress = await this.calculatePairAddress(tokenA, tokenB);
const pair = this.lensClient.getContract(
pairAddress,
'contracts/uniswap-v2/v2-core/contracts/UniswapV2Pair.sol:UniswapV2Pair'
);
await this.lensClient.contract(pair, 'approve', [router2.address, liquidity], user, undefined, false);
return await this.lensClient.contract(
router2,
'removeLiquidity',
[tokenA.address, tokenB.address, liquidity, amountAMin, amountBMin, user, deadline],
user,
undefined,
trace
);
}
EVM LENS

Function Trace

WorkflowRemove Liquidity
DescriptionSee the function trace and protocol source code
NETWORK STATUS: ONLINE
BROWSER VM