Skip to main content

Impermanent Loss Calculation

Impermanent Loss is used to described unrealized loss of liquidity due to price fluctuation on AMMs, such as Uniswap and Perp V2. As the current price of the pool diverges from the price when the liquidity was first added, the amounts of two tokens in a liquidity range becomes different from the previous states.

For example, let's assume a user Alice adds 1 ETH + 0 USDC to a range. Later, the price changes and Alice's 1 ETH + 0 USDC becomes 0 ETH + 90 USDC. If the current price of ETH is 100 USDC, this means she has 10 loss, since -1 * 100 (loss from ETH) + 90 (USDC) = -10.

However, if the current price returns to the original price, the amounts of two tokens will become identical to the original states. This is the reason for the impermanent aspect of the loss, as it is not realized until the liquidity is removed.

We have written a post for explaining it in more details, see: What is Impermanent Loss?

Impermanent Position

On Perp V2, all users, both makers and takers, operate on their positions. Thus, we refer to makers' positions as Impermanent Position due to their constantly changing size (by contrast, takers' position sizes won't change unless takers manually increase, reduce or close their positions.)

The total notional value of an impermanent positions is Impermanent Loss; in other words, Impermanent Loss is induced from Impermanent Positions on Perp V2.

How to calculate Impermanent Loss on Perp V2?

Since only makers have Impermanent Loss, we can focus on the contract OrderBook, which is responsible for liquidity, or makers' positions, or order as we call in our smart contracts.

Get Order

  • address trader: the address of the maker to query
  • address baseToken: the address of the vToken of the market, e.g. vETH or ETH market
  • int24 lowerTick: the tick of the lower range of an order
  • int24 upperTick: the tick of the upper range of an order

If we don't know the range (lowerTick & upperTick) of an order:

  1. get IDs of all orders of a maker: OrderBook.getOpenOrderIds(trader, baseToken)
  2. and get the info of a specific order using its ID: OrderBook.getOpenOrderById(bytes32 orderId)

Else, if we already know the range of an order: OrderBook.getOpenOrder(trader, baseToken, lowerTick, upperTick)

Through the two approaches above, we can get the structure of an order as OpenOrder.Info:

struct Info {
uint128 liquidity;
int24 lowerTick;
int24 upperTick;
uint256 lastFeeGrowthInsideX128;
int256 lastTwPremiumGrowthInsideX96;
int256 lastTwPremiumGrowthBelowX96;
int256 lastTwPremiumDivBySqrtPriceGrowthInsideX96;
uint256 baseDebt;
uint256 quoteDebt;
}

Get Current Amounts of Two Tokens

As Perp V2 is built on Uniswap V3, when querying the amounts of two tokens in an order, we'll be using Uniswap's contract LiquidityAmounts.sol and TickMath.sol.

First, translate ticks into square root price (sqrtPrice) with function TickMath.getSqrtRatioAtTick(tick):

  • uint160 sqrtPriceAtLowerTick = TickMath.getSqrtRatioAtTick(lowerTick)
  • uint160 sqrtPriceAtUpperTick = TickMath.getSqrtRatioAtTick(upperTick)

Then inputting the two values for sqrtPrice and liquidity above, we get from OpenOrder.Info into LiquidityAmounts.getAmount{0,1}ForLiquidity(sqrtPriceAtLowerTick, sqrtPriceAtUpperTick, liquidity)

  • baseToken: LiquidityAmounts.getAmount0ForLiquidity()
  • quoteToken: LiquidityAmounts.getAmount1ForLiquidity()

as baseToken is always token0 and quoteToken token1 on Perp V2.

However, when the current price of a pool is between sqrtPriceAtLowerTick and sqrtPriceAtUpperTick, we have to modify the code above by taking the current price sqrtMarkPriceX96 into consideration:

  • baseToken
LiquidityAmounts.getAmount0ForLiquidity(
sqrtMarkPriceX96 > sqrtPriceAtLowerTick ? sqrtMarkPriceX96 : sqrtPriceAtLowerTick,
sqrtPriceAtUpperTick,
liquidity
)
  • quoteToken
LiquidityAmounts.getAmount1ForLiquidity(
sqrtPriceAtLowerTick
sqrtMarkPriceX96 < sqrtPriceAtUpperTick ? sqrtMarkPriceX96 : sqrtPriceAtUpperTick,
liquidity
)

The reason for different parameters in the two scenarios is that baseToken gets depleted when the price goes up; thus, the first parameter as the lower range has to move accordingly. Similarly, quoteToken gets depleted when the price goes down and thus the second parameter as the upper price is dependent on the current price.

The suffix X96 in sqrtMarkPriceX96 means the value is scaled by 2^96 as designed by Uniswap V3. It can be fetched by the first return value of UniswapV3Pool.slot0().

Token Debt

Notice that in the structure OpenOrder.Info, there are baseDebt and quoteDebt.

The idea of debt is simple: the amount of token a user owes to the exchange.

Thus, baseDebt is the amount of baseToken and quoteDebt, the amount of quoteToken a user has to pay back when removing liquidity.

This value is registered when an order is initiated by ClearingHouse.addLiquidity(), e.g. if 1 ETH and 100 USDC are added, baseDebt will be 1 * 10^18 and quoteDebt 100 * 10^6, (ETH's decimals are 18 and USDC's decimals are 6).

Impermanent Loss Calculation

Now that we have

  1. the current amounts of two tokens
  2. the debt amounts of two tokens

by simply subtracting them, the difference is the Impermanent Position size.

Using the same Alice's example above, let's see what are the balance changes when her 1 ETH + 0 USDC becomes 0 ETH + 90 USDC:

Originally

  • quoteToken debt: 0 vUSD
  • baseToken debt: 1 vETH
  • current quoteToken amount: 0 vUSD
  • current baseToken amount: 1 vETH

Alice's net token amounts:

  • baseToken: 1 - 1 = 0
  • quoteToken: 0 - 10 = 0

Later when ETH price changes

  • quoteToken debt: 0 vUSD
  • baseToken debt: 1 vETH
  • current quoteToken amount: 90 vUSD
  • current baseToken amount: 0 vETH

Alice's net token amounts:

  • baseToken: 0 - 1 = -1
  • quoteToken: 90 - 0 = 90

So we can see that Alice's Impermanent Loss in this scenario is -1 * 100 (loss from ETH) + 90 (USD) = -10, or -1 * 100 + 90 * 1 = -10.

The reason is that vUSD is the settlement token, meaning that vUSD is always the quoteToken in any market on Perp V2.

Thus, all baseToken prices are denominated in vUSD, as vETH in this case is 100, since 1 vETH = 100 vUSD.

The price of vUSD, denominated in itself, is of course, 1.