Remove Liquidity
Prereq: Add Liquidity
This page covers the necessary concepts to understand the remove liquidity flow in Uniswap V2.
Concepts
Section titled “Concepts”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:145uint 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 _mintFeeamount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distributionamount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distributionIt calculates the pro-rata distribution (no dilution) using:
The calculation uses actual token balances (not just reserves), which include accumulated fees
Protocol Fees
Section titled “Protocol 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.
Formula
Section titled “Formula”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:
- = the total number of LP tokens that already exist at the fee-accounting checkpoint.
- = 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
- = liquidity measured at the previous fee-accounting checkpoint.
- = liquidity measured now, when the contract is deciding whether protocol fee LP tokens should be minted.
Therefore:
- = growth in liquidity
- = total LP supply
The protocol should get (1/6) of that growth, so after minting its share of the pool must satisfy:
mintFee
Section titled “mintFee”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:107function _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
Trading Fees
Section titled “Trading Fees”You can verify that the burn formula uses actual token balances (not just reserves), which include accumulated fees.
Function Trace
Section titled “Function Trace”In this example we will remove liquidity from a pool.
This is the code that runs to produce the function trace:
// UniswapV2Workflows:removeLiquidity:138:171async 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:255private 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 );}