Uniswap V2 is one of the most popular decentralized exchanges that relies on the Automated Market Maker (AMM) principles to facilitate interaction with decentralized pools of liquidity either by trading, or earning yield from liquidity provision.
DeFi protocols like Uniswap rely on precise mathematical models to calculate user shares, distribute profits and maintain pool balance.
Fundamental to these protocols is the concept of user shares — the proportional ownership of assets in a pool — and the mechanism of profit distribution.
In this article, I provide an in-depth explanation of the mechanisms, math, and processes used in these protocols for share, profit calculation and pool rebalancing.
Shares in DeFi protocols represent ownership or a claim to a portion of pooled assets. These shares track user contributions relative to the pool, determine user entitlement to profits. It simplifies proportional ownership in scenarios with many participants. In Uniswap V2, shares represent a user’s liquidity in token pairs.
When liquidity providers deposit tokens into a uniswap pool, they receive liquidity provider (LP) tokens as proof of ownership which entitles them to:
A share of the pool’s trading fees.
A proportional claim on the pool’s reserves or total liquidity.
The value of a pool changes based on market conditions — deposits, withdrawals, trading fees.
In a single token pool, when the total deposits match the pool’s reserves or total liquidity, the amount of shares to mint can be calculated based on ratio 1:1.
Example 1:
User A deposits 300 usdc in a pool.
User B deposits 500 usdc in a pool.
User C deposits 200 usdc in a pool.
The pool’s value is 1000 usdc.
The shares to mint is ratio-based, 1:1, because;
Total liquidity provided = The pool’s total liquidity or reserves.
The shares to mint per user = 1000 total deposits/1000 total liquidity.
Supposing each user add 1 usdc to the pool, they get 1 shares minted to them each.
Though, the shares to mint is calculated based on the users total deposits. The shares to mint of a user who provided 300 usdc to the pool will be 300 shares.
LP tokens are minted based on the ratio of the liquidity provided by the user compared to the total liquidity in the pool.
Example 2:
If the pool profits 100 usdc from a yield bearing vault, now the total deposits is not equal to the pool value since the pool has generated 100 usdc profit which totals to 1,100 usdc.
If a User D deposits 110 usdc, the total deposited changes forwad.
To calculate the shares to mint,
T — Total shares minted
S — Shares to mint
L0 — Value of the pool before deposit
L1 — Value of the pool after deposit
Unlike the previous example where no share has been minted because it was based on first deposit, we assume T > 0.
Shares to mint = L1 — L0/L0 * T
The value of the pool could be derived using the balance of tokens deposited in the pool or it could be derived in other ways since the pool could generate some yield.
The percentage of shares to mint should be proportional to the percentage increase from L0 to L1. The amount of shares to mint should equally be proportional to the change in increase.
Let us put numbers into the formula.
If L0 = 1100, L1 = 1210, T = 1000, S = ?
S = L1-L0/L0 T
1210–1100/1100 1000
Shares to mint = 100 LP tokens.
The same ratio applies to calculate token B.
Unlike the single token pool which could have a yield generation strategy contract deployed separately for it to generate profit for the pool which is proportionally distributed across all liquidity providers. Uniswap V2 generates profits through a fee-based exchange mechanism that allows a user to trade tokens which is proportionally distributed to liquidity providers.
Uniswap V2 pool is designed in such a way that its pool manages two tokens, token X and token Y, for the exchange to occur. Similar to the traditional exchange market, the exchange rate is the price of exchange for the desired token. For the exchange to occur, the two tokens must be available in Uniswap V2 pool, which is provided by a liquidity provider. The responsibility of a liquidity provider is to create a liquidity pool or add to an existing pool which is ratio-based. The idea of a ratio-based pool is that the ratio of token X and token Y in the pool is able to efficiently cater for traders who desire either token. This ratio is set when creating a liquidity pool, it is simply the amount of both tokens added at the point of creation.
Uniswap does not allow adding arbitrary amounts of tokens as liquidity unless it is proportional to the ratio of tokens in the pool. To determine the amount of liquidity to provide, let’s consider:
The pool’s reserve.
The spot price.
The pool’s reserve is the total amount of token X and token Y in a Uniswap pool sufficient to efficiently enable traders to swap. The ratio of the reserve is used to determine the price of tokens. The concept of price is such that without price you cannot know the value of something (token X or token Y) added to a pool, in other words, the price of tokens is used to determine the optimal amounts of tokens that can added to the pool.
Note: When the value(liquidity) of token X or token Y is added to a pool at a certain price, it is distributed evenly along every possible price curve.
For example, if an LP provides 10 ETH and 10,000 USDT, the liquidity is added across the entire price curve, which includes prices like:
1 ETH = 500 USDT (low price)
1 ETH = 5,000 USDT (high price)
And every possible price in between. Even if the current market price of ETH is 1 ETH = 1,000 USDT, some of the liquidity is allocated to very low prices (e.g., 1 ETH = 500 USDT) and very high prices (e.g., 1 ETH = 5,000 USDT). If trading doesn't occur at those price ranges, the liquidity spread to that price range stays at that price until a swap occur otherwise the value remain unused (fragmented).
So, to keep the price constant, since price only changes when a swap occurs and not when liquidity is provided, the ratio of liquidity to provide must equal the ratio of the pool’s reserves.
Mathematically,
Price = Token X Reserve / Token Y Reserve.
If we have 1 million dai and 2,000 eth in a pool.
Price of token Y = 1 million DAI / 2,000 ETH.
Therefore, Price of token Y in terms of token X= 500 DAI per ETH.
Note: This is the spot price of token Y in terms of token X.
The price of tokens after adding liquidity = The price of tokens before adding liquidity.
X0 — amount of token X before adding liquidity.
Y0 — amount of token Ybefore adding liquidity.
ΔX — amount of token X to add as liquidity.
ΔY — amount of token Y to add as liquidity.
ΔX/ΔY = X/Y
Uniswap operates using the constant product AMM where X * Y= K
It says, liquidity must be added in proportion to the pool’s current reserves, ensuring X and Y remain in the correct ratio. This is because any imbalance would break the constant product invariant.
Therefore, X/Y represents the spot price.
Equally, ΔX/ΔY represents the spot price since the price doesn’t change based on the given ratio applied even after adding liquidity.
The spot price is derived from the ratio of the tokens reserves. It is the price of the tokens in the Uniswap pool which does not reflect external price unless an arbitrage occurs to even the prices.
The formula shows that to keep the price constant, using the same ratio we must optimally derive how much of token X and Y to provide.
Expanding the formula, Y+ ΔY/ X + ΔX = X / Y
Y.ΔY = X.ΔX
ΔX/ΔY = X / Y satisfies the equation above.
So to calculate the amount of token X to add:
ΔX = Y.ΔY / X
using this formula, we can derive the optimal amount of token X to add to the pool.
you can find this in the uniswap code:
function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
amountB = amountA.mul(reserveB) / reserveA;
}
In a scenario where the deposited tokens or liquidity doesn’t match the correct ratio.
Uniswap adjusts your deposit to fit the correct ratio.
Uniswap will use only the amount required to match the pool ratio and refund excess amount of tokens.
You will receive fewer LP tokens since only the portion of tokens that optimally fit the correct ratio is used to provide liquidity.
Uniswap uses a math function to represent the total amount of Liquidity in the pool,
f(x,y) = L
also, uniswap uses the square of x and y to represent liquidity.
i.e. √x.y = L
in other words, f(x,y) = √x.y.
x and y — the pool reserves of tokens
L — the total liquidity tokens minted or that LPs hold altogether.
see the uniswap code:
uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
_mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
// this low-level function should be called from a contract which performs important safety checks
function mint(address to) external lock returns (uint liquidity) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
uint balance0 = IERC20(token0).balanceOf(address(this));
uint balance1 = IERC20(token1).balanceOf(address(this));
uint amount0 = balance0.sub(_reserve0);
uint amount1 = balance1.sub(_reserve1);
bool feeOn = _mintFee(_reserve0, _reserve1);
uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
_mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
} else {
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
}
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity);
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Mint(msg.sender, amount0, amount1);
}
Don’t forget — For optimal liquidity addition, the ratio of the deposited tokens must match the current pool ratio.
Δx/Δy=x/y
If √x.y = L is the total liquidity LP’s collectively hold before adding liquidity.
Then, √x1.y1 = L1 is the total liquidity LP’s collectively hold after adding liquidity.
So, L1 — L = LP’s token amount.
LP’s token amount/L1 * T = LP’s pool share.
Example 3:
If the reserves of two tokens in a pool are 1000 and 500 respectively.
The ratio is 1000:500 = 2:1
Supposing you deposit 200 amount of token X and token Y
To maintain the 2:1 ratio , token X optimally fits the pool ratio but we need 100 amount of token Y to match the ratio.
So, uniswap will calculate the amount of token Y to add to the pool optimally and refund the remaining 100 tokens.
you can find this in the uniswap code:
(uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
if (reserveA == 0 && reserveB == 0) {
(amountA, amountB) = (amountADesired, amountBDesired);
} else {
uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
if (amountBOptimal <= amountBDesired) {
require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
(amountA, amountB) = (amountADesired, amountBOptimal);
function _addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin
) internal virtual returns (uint amountA, uint amountB) {
// create the pair if it doesn't exist yet
if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
IUniswapV2Factory(factory).createPair(tokenA, tokenB);
}
(uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
if (reserveA == 0 && reserveB == 0) {
(amountA, amountB) = (amountADesired, amountBDesired);
} else {
uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
if (amountBOptimal <= amountBDesired) {
require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
(amountA, amountB) = (amountADesired, amountBOptimal);
} else {
uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
assert(amountAOptimal <= amountADesired);
require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
(amountA, amountB) = (amountAOptimal, amountBDesired);
}
}
}
The main entry point to add liquidity to Uniswap V2 pool is the addLiquidity
function in the periphery contract.
function addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
liquidity = IUniswapV2Pair(pair).mint(to);
}
Here is how it works:
- It calls the internal function _addLiquidity
explained earlier which does the ratio calculation and the optimal amounts to add.
- It transfers the tokens to the pair contract.
- It mints liquidity to the user.
Shares in Uniswap V2: Represented by LP tokens, which track user contributions to the pool.
Profit Mechanism: Profits from trading fees are embedded in the value of LP tokens.
Fee Distribution: Automatically proportional to LP tokens held.
Dynamic Rebalancing: Ensured by the constant product formula.
The Uniswap V2's mechanisms for calculating shares and distributing profits create a seamless, decentralized experience for liquidity providers. The protocol simplifies ownership tracking, and by leveraging its constant product formula ensures efficient pool rebalancing and fee collection. Understanding these mechanisms enables LPs to make informed decisions and optimize their DeFi strategies.