An exponential moving-average (EMA) price oracle with a periodicity determined by ma_time. It returns the price relative to the coin at index 0 in the pool.
Example: Price Oracle for TriCRV
The TriCRV pool consists of crvUSD <> wETH <> CRV.
Because crvUSD is coin[0], the prices of wETH and CRV are returned with regard to crvUSD.
>>>price_oracle(0)=36709495762871682546553670.94957629# price of wETH w.r.t crvUSD>>>price_oracle(1)=7249883091670510660.72498830916# price of CRV w.r.t crvUSD
In order to get the reverse EMA (e.g. price of crvUSD with regard to wETH):
The formula to calculate the exponential moving-average essentially comes down to:
Source Code for Calculating the EMA
@internaldeftweak_price(A_gamma:uint256[2],_xp:uint256[N_COINS],new_D:uint256,K0_prev:uint256=0,)->uint256:""" @notice Tweaks price_oracle, last_price and conditionally adjusts price_scale. This is called whenever there is an unbalanced liquidity operation: _exchange, add_liquidity, or remove_liquidity_one_coin. @dev Contains main liquidity rebalancing logic, by tweaking `price_scale`. @param A_gamma Array of A and gamma parameters. @param _xp Array of current balances. @param new_D New D value. @param K0_prev Initial guess for `newton_D`. """# ---------------------------- Read storage ------------------------------rebalancing_params:uint256[3]=self._unpack(self.packed_rebalancing_params)# <---------- Contains: allowed_extra_profit, adjustment_step, ma_time.price_oracle:uint256[N_COINS-1]=self._unpack_prices(self.price_oracle_packed)last_prices:uint256[N_COINS-1]=self._unpack_prices(self.last_prices_packed)packed_price_scale:uint256=self.price_scale_packedprice_scale:uint256[N_COINS-1]=self._unpack_prices(packed_price_scale)total_supply:uint256=self.totalSupplyold_xcp_profit:uint256=self.xcp_profitold_virtual_price:uint256=self.virtual_pricelast_prices_timestamp:uint256=self.last_prices_timestamp# ----------------------- Update MA if needed ----------------------------iflast_prices_timestamp<block.timestamp:# The moving average price oracle is calculated using the last_price# of the trade at the previous block, and the price oracle logged# before that trade. This can happen only once per block.# ------------------ Calculate moving average params -----------------alpha:uint256=MATH.wad_exp(-convert(unsafe_div((block.timestamp-last_prices_timestamp)*10**18,rebalancing_params[2]# <----------------------- ma_time.),int256,))forkinrange(N_COINS-1):# ----------------- We cap state price that goes into the EMA with# 2 x price_scale.price_oracle[k]=unsafe_div(min(last_prices[k],2*price_scale[k])*(10**18-alpha)+price_oracle[k]*alpha,# ^-------- Cap spot price into EMA.10**18)self.price_oracle_packed=self._pack_prices(price_oracle)self.last_prices_timestamp=block.timestamp# <---- Store timestamp.# price_oracle is used further on to calculate its vector# distance from price_scale. This distance is used to calculate# the amount of adjustment to be done to the price_scale.# ------------------ If new_D is set to 0, calculate it ------------------D_unadjusted:uint256=new_Difnew_D==0:# <--------------------------- _exchange sets new_D to 0.D_unadjusted=MATH.newton_D(A_gamma[0],A_gamma[1],_xp,K0_prev)# ----------------------- Calculate last_prices --------------------------last_prices=MATH.get_p(_xp,D_unadjusted,A_gamma)forkinrange(N_COINS-1):last_prices[k]=unsafe_div(last_prices[k]*price_scale[k],10**18)self.last_prices_packed=self._pack_prices(last_prices)# ---------- Update profit numbers without price adjustment first --------xp:uint256[N_COINS]=empty(uint256[N_COINS])xp[0]=unsafe_div(D_unadjusted,N_COINS)forkinrange(N_COINS-1):xp[k+1]=D_unadjusted*10**18/(N_COINS*price_scale[k])# ------------------------- Update xcp_profit ----------------------------xcp_profit:uint256=10**18virtual_price:uint256=10**18ifold_virtual_price>0:xcp:uint256=MATH.geometric_mean(xp)virtual_price=10**18*xcp/total_supplyxcp_profit=unsafe_div(old_xcp_profit*virtual_price,old_virtual_price)# <---------------- Safu to do unsafe_div as old_virtual_price > 0.# If A and gamma are not undergoing ramps (t < block.timestamp),# ensure new virtual_price is not less than old virtual_price,# else the pool suffers a loss.ifself.future_A_gamma_time<block.timestamp:assertvirtual_price>old_virtual_price,"Loss"self.xcp_profit=xcp_profit# ------------ Rebalance liquidity if there's enough profits to adjust it:ifvirtual_price*2-10**18>xcp_profit+2*rebalancing_params[0]:# allowed_extra_profit --------^# ------------------- Get adjustment step ----------------------------# Calculate the vector distance between price_scale and# price_oracle.norm:uint256=0ratio:uint256=0forkinrange(N_COINS-1):ratio=unsafe_div(price_oracle[k]*10**18,price_scale[k])# unsafe_div because we did safediv before ----^ifratio>10**18:ratio=unsafe_sub(ratio,10**18)else:ratio=unsafe_sub(10**18,ratio)norm=unsafe_add(norm,ratio**2)norm=isqrt(norm)# <-------------------- isqrt is not in base 1e18.adjustment_step:uint256=max(rebalancing_params[1],unsafe_div(norm,5))# ^------------------------------------- adjustment_step.ifnorm>adjustment_step:# <---------- We only adjust prices if the# vector distance between price_oracle and price_scale is# large enough. This check ensures that no rebalancing# occurs if the distance is low i.e. the pool prices are# pegged to the oracle prices.# ------------------------------------- Calculate new price scale.p_new:uint256[N_COINS-1]=empty(uint256[N_COINS-1])forkinrange(N_COINS-1):p_new[k]=unsafe_div(price_scale[k]*unsafe_sub(norm,adjustment_step)+adjustment_step*price_oracle[k],norm)# <- norm is non-zero and gt adjustment_step; unsafe = safe# ---------------- Update stale xp (using price_scale) with p_new.xp=_xpforkinrange(N_COINS-1):xp[k+1]=unsafe_div(_xp[k+1]*p_new[k],price_scale[k])# unsafe_div because we did safediv before ----^# ------------------------------------------ Update D with new xp.D:uint256=MATH.newton_D(A_gamma[0],A_gamma[1],xp,0)forkinrange(N_COINS):frac:uint256=xp[k]*10**18/D# <----- Check validity ofassert(frac>10**16-1)and(frac<10**20+1)# p_new.xp[0]=D/N_COINSforkinrange(N_COINS-1):xp[k+1]=D*10**18/(N_COINS*p_new[k])# <---- Convert# xp to real prices.# ---------- Calculate new virtual_price using new xp and D. Reuse# `old_virtual_price` (but it has new virtual_price).old_virtual_price=unsafe_div(10**18*MATH.geometric_mean(xp),total_supply)# <----- unsafe_div because we did safediv before (if vp>1e18)# ---------------------------- Proceed if we've got enough profit.if(old_virtual_price>10**18and2*old_virtual_price-10**18>xcp_profit):packed_price_scale=self._pack_prices(p_new)self.D=Dself.virtual_price=old_virtual_priceself.price_scale_packed=packed_price_scalereturnpacked_price_scale# --------- price_scale was not adjusted. Update the profit counter and D.self.D=D_unadjustedself.virtual_price=virtual_pricereturnpacked_price_scale
Note: The state price that goes into the EMA is capped with 2 x price_scale to prevent manipulation.
Variable
Description
block.timestamp
Timestamp of the block. Since all transactions within a block share the same timestamp, EMA oracles can only be updated once per block.
last_prices_timestamp
Timestamp when the EMA oracle was last updated.
ma_time
Time window for the moving-average oracle.
last_prices
Last stored spot price of the coin to calculate the price oracle for.
price_scale
Price scale value of the coin to calculate the price oracle for.
price_oracle
Price oracle value of the coin to calculate the price oracle for.
alpha
Weighting multiplier that adjusts the impact of the latest spot value versus the previous EMA in the new EMA calculation.
The AMM implementation uses several private variables to pack and store key values, which are used for calculating the EMA oracle.
Info
Some storage variables pack multiple values into a single entry to save on gas costs. These values are unpacked when needed for use.
Source Code
PRICE_SIZE:constant(uint128)=256/(N_COINS-1)PRICE_MASK:constant(uint256)=2**PRICE_SIZE-1last_prices_packed:uint256@internal@viewdef_pack_prices(prices_to_pack:uint256[N_COINS-1])->uint256:""" @notice Packs N_COINS-1 prices into a uint256. @param prices_to_pack The prices to pack @return uint256 An integer that packs prices """packed_prices:uint256=0p:uint256=0forkinrange(N_COINS-1):packed_prices=packed_prices<<PRICE_SIZEp=prices_to_pack[N_COINS-2-k]assertp<PRICE_MASKpacked_prices=p|packed_pricesreturnpacked_prices
PRICE_SIZE:constant(uint128)=256/(N_COINS-1)PRICE_MASK:constant(uint256)=2**PRICE_SIZE-1last_prices_packed:uint256@internal@viewdef_unpack_prices(_packed_prices:uint256)->uint256[2]:""" @notice Unpacks N_COINS-1 prices from a uint256. @param _packed_prices The packed prices @return uint256[2] Unpacked prices """unpacked_prices:uint256[N_COINS-1]=empty(uint256[N_COINS-1])packed_prices:uint256=_packed_pricesforkinrange(N_COINS-1):unpacked_prices[k]=packed_prices&PRICE_MASKpacked_prices=packed_prices>>PRICE_SIZEreturnunpacked_prices
last_prices_packed stores the latest prices of all coins, packaging them into a single variable. When using the prices, they must be unpacked using the _unpack_prices method.
price_oracle_packed stores the moving average values of the coins. This variable includes two values: the first represents the moving average for coin(1) relative to coin(0), and the second represents the moving average for coin(2) relative to coin(0). The price_oracle method unpacks these values and return the price oracle of the coin at index k.
price_scale_packed functions similarly by packing the price scales of coin 1 and 2 with respect to coin 0. The price_scale method unpacks these values and returns the price scale of the coin at index k.
last_prices_timestamp marks the timestamp when the price_oracle for a coin was last updated.
Function to calculate the exponential moving average (EMA) price for the coin at index k with regard to the coin at index 0. The oracle is an exponential moving average, with a periodicity determined by ma_time. The aggregated prices are cached state prices (dy/dx) calculated AFTER the latest trade.
The moving average price oracle is calculated using the last_price of the trade at the previous block, and the price oracle logged before that trade. This can happen only once per block.
Returns: EMA price of coin k (uint256).
Input
Type
Description
k
uint256
Index of the coin.
Source code
price_scale_packed:uint256# <------------------------ Internal price scale.price_oracle_packed:uint256# <------- Price target given by moving average.last_prices_packed:uint256last_prices_timestamp:public(uint256)@external@view@nonreentrant("lock")defprice_oracle(k:uint256)->uint256:""" @notice Returns the oracle price of the coin at index `k` w.r.t the coin at index 0. @dev The oracle is an exponential moving average, with a periodicity determined by `self.ma_time`. The aggregated prices are cached state prices (dy/dx) calculated AFTER the latest trade. @param k The index of the coin. @return uint256 Price oracle value of kth coin. """price_oracle:uint256=self._unpack_prices(self.price_oracle_packed)[k]price_scale:uint256=self._unpack_prices(self.price_scale_packed)[k]last_prices_timestamp:uint256=self.last_prices_timestampiflast_prices_timestamp<block.timestamp:# <------------ Update moving# average if needed.last_prices:uint256=self._unpack_prices(self.last_prices_packed)[k]ma_time:uint256=self._unpack(self.packed_rebalancing_params)[2]alpha:uint256=MATH.wad_exp(-convert((block.timestamp-last_prices_timestamp)*10**18/ma_time,int256,))# ---- We cap state price that goes into the EMA with 2 x price_scale.return(min(last_prices,2*price_scale)*(10**18-alpha)+price_oracle*alpha)/10**18returnprice_oracle
Getter for the price scale of the coin at index k.
Returns: price scale of the coin k (uint256).
Input
Type
Description
k
uint256
Index of the coin.
Source code
price_scale_packed:uint256# <------------------------ Internal price scale.@external@viewdefprice_scale(k:uint256)->uint256:""" @notice Returns the price scale of the coin at index `k` w.r.t the coin at index 0. @dev Price scale determines the price band around which liquidity is concentrated. @param k The index of the coin. @return uint256 Price scale of coin. """returnself._unpack_prices(self.price_scale_packed)[k]
Getter method for the last stored price for coin at index value k, stored in last_prices_packed.
Returns: last stored spot price of coin k (uint256).
Input
Type
Description
k
uint256
Index of the coin.
Source code
last_prices_packed:uint256@external@viewdeflast_prices(k:uint256)->uint256:""" @notice Returns last price of the coin at index `k` w.r.t the coin at index 0. @dev last_prices returns the quote by the AMM for an infinitesimally small swap after the last trade. It is not equivalent to the last traded price, and is computed by taking the partial differential of `x` w.r.t `y`. The derivative is calculated in `get_p` and then multiplied with price_scale to give last_prices. @param k The index of the coin. @return uint256 Last logged price of coin. """returnself._unpack_prices(self.last_prices_packed)[k]@internal@viewdef_unpack_prices(_packed_prices:uint256)->uint256[2]:""" @notice Unpacks N_COINS-1 prices from a uint256. @param _packed_prices The packed prices @return uint256[2] Unpacked prices """unpacked_prices:uint256[N_COINS-1]=empty(uint256[N_COINS-1])packed_prices:uint256=_packed_pricesforkinrange(N_COINS-1):unpacked_prices[k]=packed_prices&PRICE_MASKpacked_prices=packed_prices>>PRICE_SIZEreturnunpacked_prices
Getter for the exponential moving average time for the price oracle. This value can be adjusted via commit_new_parameters(), as detailed in the admin controls section.
Returns: periodicity of the EMA (uint256)
Source code
packed_rebalancing_params:public(uint256)# <---------- Contains rebalancing# parameters allowed_extra_profit, adjustment_step, and ma_time.@view@externaldefma_time()->uint256:""" @notice Returns the current moving average time in seconds @dev To get time in seconds, the parameter is multipled by ln(2) One can expect off-by-one errors here. @return uint256 ma_time value. """returnself._unpack(self.packed_rebalancing_params)[2]*694/1000
Getter for the price of the LP token, calculated as follows:
Returns: LP token price (uint256).
Source code
price_oracle_packed:uint256# <------- Price target given by moving average.@external@view@nonreentrant("lock")deflp_price()->uint256:""" @notice Calculates the current price of the LP token w.r.t coin at the 0th index @return uint256 LP price. """price_oracle:uint256[N_COINS-1]=self._unpack_prices(self.price_oracle_packed)return(3*self.virtual_price*MATH.cbrt(price_oracle[0]*price_oracle[1]))/10**24
Function to calculate the current virtual price of the pool 's LP token.
Returns: virtual price (uint256).
Source code
totalSupply:public(uint256)@external@view@nonreentrant("lock")defget_virtual_price()->uint256:""" @notice Calculates the current virtual price of the pool LP token. @dev Not to be confused with `self.virtual_price` which is a cached virtual price. @return uint256 Virtual Price. """return10**18*self.get_xcp(self.D)/self.totalSupply
The EMA oracle and other storage variables are updated each time the internal tweak_price function is called. This function tweaksprice_oracle and last_price, and conditionally adjusts price_scale.
The tweak_price function is called whenever there is an unbalanced liquidity operation, including:
_exchange
add_liquidity
remove_liquidity_one_coin
Source Code
@internaldeftweak_price(A_gamma:uint256[2],_xp:uint256[N_COINS],new_D:uint256,K0_prev:uint256=0,)->uint256:""" @notice Tweaks price_oracle, last_price and conditionally adjusts price_scale. This is called whenever there is an unbalanced liquidity operation: _exchange, add_liquidity, or remove_liquidity_one_coin. @dev Contains main liquidity rebalancing logic, by tweaking `price_scale`. @param A_gamma Array of A and gamma parameters. @param _xp Array of current balances. @param new_D New D value. @param K0_prev Initial guess for `newton_D`. """# ---------------------------- Read storage ------------------------------rebalancing_params:uint256[3]=self._unpack(self.packed_rebalancing_params)# <---------- Contains: allowed_extra_profit, adjustment_step, ma_time.price_oracle:uint256[N_COINS-1]=self._unpack_prices(self.price_oracle_packed)last_prices:uint256[N_COINS-1]=self._unpack_prices(self.last_prices_packed)packed_price_scale:uint256=self.price_scale_packedprice_scale:uint256[N_COINS-1]=self._unpack_prices(packed_price_scale)total_supply:uint256=self.totalSupplyold_xcp_profit:uint256=self.xcp_profitold_virtual_price:uint256=self.virtual_pricelast_prices_timestamp:uint256=self.last_prices_timestamp# ----------------------- Update MA if needed ----------------------------iflast_prices_timestamp<block.timestamp:# The moving average price oracle is calculated using the last_price# of the trade at the previous block, and the price oracle logged# before that trade. This can happen only once per block.# ------------------ Calculate moving average params -----------------alpha:uint256=MATH.wad_exp(-convert(unsafe_div((block.timestamp-last_prices_timestamp)*10**18,rebalancing_params[2]# <----------------------- ma_time.),int256,))forkinrange(N_COINS-1):# ----------------- We cap state price that goes into the EMA with# 2 x price_scale.price_oracle[k]=unsafe_div(min(last_prices[k],2*price_scale[k])*(10**18-alpha)+price_oracle[k]*alpha,# ^-------- Cap spot price into EMA.10**18)self.price_oracle_packed=self._pack_prices(price_oracle)self.last_prices_timestamp=block.timestamp# <---- Store timestamp.# price_oracle is used further on to calculate its vector# distance from price_scale. This distance is used to calculate# the amount of adjustment to be done to the price_scale.# ------------------ If new_D is set to 0, calculate it ------------------D_unadjusted:uint256=new_Difnew_D==0:# <--------------------------- _exchange sets new_D to 0.D_unadjusted=MATH.newton_D(A_gamma[0],A_gamma[1],_xp,K0_prev)# ----------------------- Calculate last_prices --------------------------last_prices=MATH.get_p(_xp,D_unadjusted,A_gamma)forkinrange(N_COINS-1):last_prices[k]=unsafe_div(last_prices[k]*price_scale[k],10**18)self.last_prices_packed=self._pack_prices(last_prices)# ---------- Update profit numbers without price adjustment first --------xp:uint256[N_COINS]=empty(uint256[N_COINS])xp[0]=unsafe_div(D_unadjusted,N_COINS)forkinrange(N_COINS-1):xp[k+1]=D_unadjusted*10**18/(N_COINS*price_scale[k])# ------------------------- Update xcp_profit ----------------------------xcp_profit:uint256=10**18virtual_price:uint256=10**18ifold_virtual_price>0:xcp:uint256=MATH.geometric_mean(xp)virtual_price=10**18*xcp/total_supplyxcp_profit=unsafe_div(old_xcp_profit*virtual_price,old_virtual_price)# <---------------- Safu to do unsafe_div as old_virtual_price > 0.# If A and gamma are not undergoing ramps (t < block.timestamp),# ensure new virtual_price is not less than old virtual_price,# else the pool suffers a loss.ifself.future_A_gamma_time<block.timestamp:assertvirtual_price>old_virtual_price,"Loss"self.xcp_profit=xcp_profit# ------------ Rebalance liquidity if there's enough profits to adjust it:ifvirtual_price*2-10**18>xcp_profit+2*rebalancing_params[0]:# allowed_extra_profit --------^# ------------------- Get adjustment step ----------------------------# Calculate the vector distance between price_scale and# price_oracle.norm:uint256=0ratio:uint256=0forkinrange(N_COINS-1):ratio=unsafe_div(price_oracle[k]*10**18,price_scale[k])# unsafe_div because we did safediv before ----^ifratio>10**18:ratio=unsafe_sub(ratio,10**18)else:ratio=unsafe_sub(10**18,ratio)norm=unsafe_add(norm,ratio**2)norm=isqrt(norm)# <-------------------- isqrt is not in base 1e18.adjustment_step:uint256=max(rebalancing_params[1],unsafe_div(norm,5))# ^------------------------------------- adjustment_step.ifnorm>adjustment_step:# <---------- We only adjust prices if the# vector distance between price_oracle and price_scale is# large enough. This check ensures that no rebalancing# occurs if the distance is low i.e. the pool prices are# pegged to the oracle prices.# ------------------------------------- Calculate new price scale.p_new:uint256[N_COINS-1]=empty(uint256[N_COINS-1])forkinrange(N_COINS-1):p_new[k]=unsafe_div(price_scale[k]*unsafe_sub(norm,adjustment_step)+adjustment_step*price_oracle[k],norm)# <- norm is non-zero and gt adjustment_step; unsafe = safe# ---------------- Update stale xp (using price_scale) with p_new.xp=_xpforkinrange(N_COINS-1):xp[k+1]=unsafe_div(_xp[k+1]*p_new[k],price_scale[k])# unsafe_div because we did safediv before ----^# ------------------------------------------ Update D with new xp.D:uint256=MATH.newton_D(A_gamma[0],A_gamma[1],xp,0)forkinrange(N_COINS):frac:uint256=xp[k]*10**18/D# <----- Check validity ofassert(frac>10**16-1)and(frac<10**20+1)# p_new.xp[0]=D/N_COINSforkinrange(N_COINS-1):xp[k+1]=D*10**18/(N_COINS*p_new[k])# <---- Convert# xp to real prices.# ---------- Calculate new virtual_price using new xp and D. Reuse# `old_virtual_price` (but it has new virtual_price).old_virtual_price=unsafe_div(10**18*MATH.geometric_mean(xp),total_supply)# <----- unsafe_div because we did safediv before (if vp>1e18)# ---------------------------- Proceed if we've got enough profit.if(old_virtual_price>10**18and2*old_virtual_price-10**18>xcp_profit):packed_price_scale=self._pack_prices(p_new)self.D=Dself.virtual_price=old_virtual_priceself.price_scale_packed=packed_price_scalereturnpacked_price_scale# --------- price_scale was not adjusted. Update the profit counter and D.self.D=D_unadjustedself.virtual_price=virtual_pricereturnpacked_price_scale