Fair Pricing of Velodrome's Stable Swap Pools

Published On:

VMEX aims to be the central hub for users to borrow tokens with their yield-earning LP and vault tokens as leverage. As such, the ability to calculate fair and hack-resistant LP token pricing is critical to the health of the protocol. In the past, there have been numerous hacks targeting the malleability of LP token prices due to swaps along the AMM curve. For instance, Warp Finance was hacked for about 7.8M of DAI due to an attacker taking out a flashloan to swap into an AMM pool, arbitrarily raising the value of the LP token as collateral, and then draining the borrowable assets.

In response, Alpha Homora has pioneered the idea of “fair reserves,” which assumes that the fair price of the LP token should not change as tokens are swapped along the curve, but rather depend on K, the invariant of the AMM curve function. They showed that their pricing formula works for Uniswap V2 AMMs, and the idea was later applied to Balancer pools. However, the formulas derived in the aforementioned articles only apply to the specific invariant curves of the AMM protocol. There did not yet exist a method to price LP tokens with solidly’s r0r13+r1r03=Kr_0r_1^3 + r_1r_0^3 = K invariant, which is the same invariant used by Velodrome for their stable pairs. Therefore, to be able to fairly price the Velodrome stable pairs, we at VMEX finance have developed the following formula.


In the above formula, K is the invariant constant, p0 is the fair price of token 0, p1 is the fair price of token 1, and L is the total supply of LP tokens.

The proof for the formula is as follows:

First, let's write the invariant (1) where r0 and r1 are the reserve amounts, or the current amount of token 0 and token 1 in the pool, respectively

(1)r0r13+r1r03=K(1)\quad r_0r_1^3 + r_1r_0^3 = K

Next, let's write the condition for fair reserves (2)

(2)p0r0=p1r1(2)\quad p_0r_0' = p_1r_1'

In this formula, p0p_0 and p1p_1 are the "fair" prices of tokens 0 and 1. The fair price would be the price returned from Chainlink, or some other central source of truth, not the current price that the AMM is reporting. We also denote the reserves in the pool r0r_0' and r1r_1' with the prime since it represents the exact amounts of each token in the pool such that the reserves amounts are fair.

Rearranging the terms in (2) we get the following

(3)r1=p0r0p1(3)\quad r_1' = \frac{p_0r_0'}{p_1}

Now, plugging in (3) to (1) and simplifying, we get the below formulas

r03p0r0p1+(p0r0p1)3r0=Kr_0'^3\frac{p_0r_0'}{p_1} + \left(\frac{p_0r_0'}{p_1}\right)^3r_0' = K

p0p1r04+(p0p1)3r04=K\frac{p_0}{p_1}r_0'^4 + \left(\frac{p_0}{p_1}\right)^3r_0'^4 = K

r04(p0p1+(p0p1)3)=Kr_0'^4 \left(\frac{p_0}{p_1} + (\frac{p_0}{p_1})^3\right) = K

(4)r0=Kp13p03+p0p124(4)\quad r_0' =\sqrt[4]{\frac{Kp_1^3}{p_0^3+p_0p_1^2}}

By symmetry of r0r_0 and r1r_1, we can also derive

(5)r1=Kp03p13+p1p024(5)\quad r_1' =\sqrt[4]{\frac{Kp_0^3}{p_1^3+p_1p_0^2}}

Now, the equation to calculate the price per token would be the total value locked in the pool divided by the total supply of LP tokens (L), shown in equation (6)

(6)p0r0+p1r1L(6)\quad \frac{p_0r_0'+p_1r_1'}{L}

Plugging in r0r_0' and r1r_1' from equations (4) and (5) into equation (6), and simplifying, we are left with the final equation for the price of a stable swap LP token:



Note: returned price should have same number of decimals as chainlink (8 if OP, 18 on mainnet)


     * @dev Gets an asset price for a velodrome token
     * @param asset The asset address
    function getVeloPrice(
        address asset
    ) internal returns (uint256 price) {
        uint256[] memory prices = new uint256[](2);

        (address token0, address token1) = IVeloPair(asset).tokens();
		(, , , , bool stable, , ) = IVeloPair(asset).metadata();  

        if(token0 == ETH_NATIVE){
            token0 = WETH;
        prices[0] = getAssetPrice(token0); //handles case where underlying is curve too.
        require(prices[0] != 0, Errors.VO_UNDERLYING_FAIL);

        if(token1 == ETH_NATIVE){
            token1 = WETH;
        prices[1] = getAssetPrice(token1); //handles case where underlying is curve too.
        require(prices[1] != 0, Errors.VO_UNDERLYING_FAIL);

        price = VelodromeOracle.get_lp_price(asset, prices, BASE_CURRENCY_DECIMALS, stable); //has 18 decimals

        if(price == 0){
            return _fallbackOracle.getAssetPrice(asset);

        return price;


// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

import {vMath} from "./libs/vMath.sol";
import {FixedPointMathLib} from "../../dependencies/solmate/FixedPointMathLib.sol"; 
import {IVeloPair} from "../../interfaces/IVeloPair.sol"; 
import {IERC20} from "../../interfaces/IERC20WithPermit.sol"; 

library VelodromeOracle {
	using FixedPointMathLib for *; 
     * @dev Gets the price of a velodrome lp token
     * @param lp_token The lp token address
     * @param prices The prices of the underlying in the liquidity pool, there must be 18 decimals
	 * @param stable is the pair stable or volatile
	//@dev assumes oracles only pass in wad scaled decimals for ALL prices
	function get_lp_price(address lp_token, uint256[] memory prices, uint256 priceDecimals, bool stable) internal view returns(uint256) {
		IVeloPair token = IVeloPair(lp_token); 	
		uint256 total_supply = IERC20(lp_token).totalSupply()* 1e18 / (10**IERC20(lp_token).decimals()); //force to be 18 decimals
		(uint256 d0, uint256 d1, uint256 r0, uint256 r1, , ,) = token.metadata(); 

		r0 *= 1e18 / d0; 
		r1 *= 1e18 / d1; 
		if (stable) {
			return calculate_stable_lp_token_price(
		} else {
			return calculate_lp_token_price(
	//where total supply is the total supply of the LP token
	//formula solves xy = k curves only
	//assumes that prices passed in are already properly WAD scaled
	function calculate_lp_token_price(
		uint256 total_supply,
		uint256 price0,
		uint256 price1,
		uint256 reserve0,
		uint256 reserve1
	) internal pure returns (uint256) {
		uint256 a = vMath.nthroot(2, reserve0 * reserve1); //ends up with same number of decimals as reserve0 or reserve1 without loss of precision, which should be 18
		uint256 b = vMath.nthroot(2, price0 * price1); //same number of dec as price0 or price1, should be chainlink agg (op: 8)
		uint256 c = 2 * a * b / total_supply; //must end up as num decimals as prices since total supply is guaranteed to be 18

		return c; 
	//solves for cases where curve is x^3 * y + y^3 * x = k  
	//fair reserves math formula author: @ksyao2002
	function calculate_stable_lp_token_price(
		uint256 total_supply,
		uint256 price0,
		uint256 price1,
		uint256 reserve0,
		uint256 reserve1,
		uint256 priceDecimals
	) internal pure returns (uint256) {
		uint256 k = getK(reserve0, reserve1); 
		//fair_reserves = ( (k * (price0 ** 3) * (price1 ** 3)) )^(1/4) / ((price0 ** 2) + (price1 ** 2));  
		price0 *= 1e18 / (10**priceDecimals); //convert to 18 dec
		price1 *= 1e18 / (10**priceDecimals); 
		uint256 a = FixedPointMathLib.rpow(price0, 3, 1e18); //keep same decimals as chainlink
		uint256 b = FixedPointMathLib.rpow(price1, 3, 1e18);  
		uint256 c = FixedPointMathLib.rpow(price0, 2, 1e18);  
		uint256 d = FixedPointMathLib.rpow(price1, 2, 1e18);  

		uint256 p0 =k * FixedPointMathLib.mulWadDown(a, b); //2*18 decimals
		uint256 fair = p0 / (c + d); // number of decimals is 18

		// each sqrt divides the num decimals by 2. So need to replenish the decimals midway through with another 1e18
		uint256 frth_fair = FixedPointMathLib.sqrt(FixedPointMathLib.sqrt(fair  * 1e18) * 1e18); // number of decimals is 18
		return 2 * ((frth_fair * (10**priceDecimals) ) / total_supply); // converts to chainlink decimals

	function getK(uint256 x, uint256 y) internal pure returns (uint256) {
		//x, n, scalar	
		uint256 x_cubed = FixedPointMathLib.rpow(x, 3, 1e18); 
		uint256 newX = FixedPointMathLib.mulWadDown(x_cubed, y); 
		uint256 y_cubed = FixedPointMathLib.rpow(y, 3, 1e18); 
		uint256 newY = FixedPointMathLib.mulWadDown(y_cubed, x); 
		return newX + newY;  //18 decimals


Volatile Labs © 2023