Because USDC is the coin at index 0, price_oracle() returns the price of crvUSD with regard to USDC.
>>>price_oracle()=9990433031855912830.99904330318# price of crvUSD w.r.t USDC
In order to get the reverse EMA (price of USDC with regard to crvUSD):
The AMM implementation utilizes two private variables, last_prices_packed and last_D_packed, to store the latest spot and EMA values. These values serve as the foundation for calculating the oracles.
Oracle Manipulation Risk
The spot price cannot be immediately used for the calculation of the moving average, as this would permit single-block oracle manipulation. Consequently, the _calc_moving_average method, which calculates the moving average of the oracle, uses last_prices_packed or last_D_packed. These variables retain prices from previous actions.
_calc_moving_average
@internal@viewdef_calc_moving_average(packed_value:uint256,averaging_window:uint256,ma_last_time:uint256)->uint256:last_spot_value:uint256=packed_value&(2**128-1)last_ema_value:uint256=(packed_value>>128)ifma_last_time<block.timestamp:# calculate new_ema_value and return that.alpha:uint256=self.exp(-convert(unsafe_div(unsafe_mul(unsafe_sub(block.timestamp,ma_last_time),10**18),averaging_window),int256))returnunsafe_div(last_spot_value*(10**18-alpha)+last_ema_value*alpha,10**18)returnlast_ema_value
The formula to calculate the exponential moving-average essentially comes down to:
with:
Variable
Description
block.timestamp
Timestamp of the block. Since all transactions within a block share the same timestamp, the EMA oracles can only be updated once per block.
last_prices_timestamp
Last time the ma oracle was updated. Differentiates between D and price.
ma_time
Time window for the moving-average oracle; for the price_oracle it's ma_exp_time, and for the D_oracle it's D_ma_time.
last_spot_value
Last price within the AMM; for the price_oracle it's last_price, which is the first value of last_prices_packed. For calculating D_oracle, it's last_D, which is the first value in last_D_packed.
last_ema_value
Last EMA value; for calculating price_oracle it's ma_price, which is the second value packed in last_prices_packed. For calculating D_oracle it's ma_D, also the second value in last_D_packed.
alpha
Weighting multiplier that adjusts the impact of the latest spot value versus the previous EMA value in the new EMA calculation.
exp
Function that calculates the natural exponential function of a signed integer with a precision of 1e18.
price_oracle calculation is based on the two values stored in last_prices_packed, last_price and ema_price. These values are conditionally updated: Generally speaking, both values are simultaneously updated whenever upkeep_oracles is called. This happens at certain actions, see here.
While last_price (spot price) is always updated at every relevant action, the ema_price is maximally updated once per block. There might be the case that there is more than one relevant action within the same block. Let's say there are two relevant actions within the block which would update both values: If this is the case, last_price is updated at every action, so there will be two updated. ema_price on the other hand will only be updated once (at the first action) and will not change a second time. Reasoning behind this is to prevent single-block manipulation. The ema_price will just be updated at the next action outside of this block.
D_oracle calculation is based on the two values stored in last_D_packed, last_D and ma_D.
Jupyter Notebook
For a practical demonstration of how individual variables behave during the upkeep of the oracle, a Jupyter notebook is available for reference. This notebook provides a plot showcasing the dynamics in the process.
Function to calculate the exponential moving average (EMA) price for the coin at index i with regard to the coin at index 0. The calculation is based on the last spot value (last_price), the last ma value (ema_price), the moving average time window (ma_exp_time), and on the difference between the current timestamp (block.timestamp) and the timestamp when the ma oracle was last updated (unpacks from the first value of ma_last_time).
i = 0 will return the price oracle of coin[1], i = 1 the price oracle of coin[2], and so on.
Returns: EMA price of coin i (uint256).
Input
Type
Description
i
uint256
Index value of the coin to calculate the EMA price for. i = 0 returns the price oracle for coin(1).
Function to calculate the exponential moving average (EMA) value for the D invariant, distinct from calculations for individual coins. This is based on the most recent "spot" value and EMA value of D, extracted from the private last_D_packed variable. It considers the moving average time window for D (D_ma_time), and calculates the difference between the current timestamp (block.timestamp) and the timestamp of the last update to the ma oracle of D, derived from the second value in ma_last_time.
Getter method for the last stored price for the coin at index value i, stored in last_prices_packed. The spot price is retrieved from the lower 128 bits of the packed value in last_prices_packed and is updated whenever the internal upkeep_oracles method is called.
i = 0 will return the last price of coin[1], i = 1 the last price of coin[2], and so on.
Returns: last stored spot price of coin i (uint256).
Input
Type
Description
i
uint256
Index value of the coin to get the last price for.
Getter method for the last stored exponential moving-average (EMA) price of the coin at index value i, retrieved from last_prices_packed. The EMA price is obtained by shifting the value in last_prices_packed to the right by 128 bits. This value is updated whenever the upkeep_oracles() function is internally called.
i = 0 will return the last EMA price of coin[1], i = 1 of coin[2], and so on.
Returns: the last stored EMA price of coin i (uint256).
Input
Type
Description
i
uint256
Index of the coin for which to retrieve the last EMA price.
Function to calculate the current AMM spot price of coin i based on the coin balances in the pool, the amplification coefficient A, and the D invariant.
i = 0 will return the price of coin[1], i = 1 the price of coin[2], and so on.
Returns: current spot price (uint256).
Input
Type
Description
i
uint256
Index of the coin for which to calculate the current spot price.
Source code
@external@viewdefget_p(i:uint256)->uint256:""" @notice Returns the AMM State price of token @dev if i = 0, it will return the state price of coin[1]. @param i index of state price (0 for coin[1], 1 for coin[2], ...) @return uint256 The state price quoted by the AMM for coin[i+1] """amp:uint256=self._A()xp:DynArray[uint256,MAX_COINS]=self._xp_mem(self._stored_rates(),self._balances())D:uint256=self.get_D(xp,amp)returnself._get_p(xp,amp,D)[i]@internal@puredef_get_p(xp:DynArray[uint256,MAX_COINS],amp:uint256,D:uint256,)->DynArray[uint256,MAX_COINS]:# dx_0 / dx_1 only, however can have any number of coins in poolANN:uint256=unsafe_mul(amp,N_COINS)Dr:uint256=unsafe_div(D,pow_mod256(N_COINS,N_COINS))foriinrange(MAX_COINS_128):ifi==N_COINS_128:breakDr=Dr*D/xp[i]p:DynArray[uint256,MAX_COINS]=empty(DynArray[uint256,MAX_COINS])xp0_A:uint256=ANN*xp[0]/A_PRECISIONforiinrange(1,MAX_COINS):ifi==N_COINS:breakp.append(10**18*(xp0_A+Dr*xp[0]/xp[i])/(xp0_A+Dr))returnp
This method may be vulnerable to donation-style attacks if the implementation contains rebasing tokens. For integrators, caution is advised.
Getter for the current virtual price of the LP token, which represents a price relative to the underlying.
Returns: virtual price (uint256).
Source code
@view@external@nonreentrant('lock')defget_virtual_price()->uint256:""" @notice The current virtual price of the pool LP token @dev Useful for calculating profits. The method may be vulnerable to donation-style attacks if implementation contains rebasing tokens. For integrators, caution is advised. @return LP token virtual price normalized to 1e18 """amp:uint256=self._A()xp:DynArray[uint256,MAX_COINS]=self._xp_mem(self._stored_rates(),self._balances())D:uint256=self.get_D(xp,amp)# D is in the units similar to DAI (e.g. converted to precision 1e18)# When balanced, D = n * x_u - total virtual value of the portfolioreturnD*PRECISION/self.total_supply
Getter for the exponential moving-average time for the price oracle (price_oracle). This value can be adjusted via set_ma_exp_time(), as detailed in the admin controls section.
This variable contains two packed values because there needs to be a distinction between prices and the D invariant. The reasoning behind this is that the moving-average price oracle is not updated if users remove liquidity in a balanced proportion (remove_liquidity), but the D oracle is.
Getter for the last time the exponential moving-average oracle of coin prices or the D invariant was updated. This variable contains two packed values: ma_last_time_p, which represents the timestamp of the last update for prices, and ma_last_time_D, which represents the last timestamp of the oracle update for the D invariant.
The value needs to be unpacked, as it contains two values, ma_last_time_p and ma_last_time_D.
For example, 579359617954437487117250992339883299967854142015 is unpacked into two uint256 numbers. First, its lower 128 bits are isolated using a bitwise AND with 2**128 − 1, and then the value is shifted right by 128 bits to extract the upper 128 bits. It returns: [1702584895, 1702584895], meaning both moving-average oracles were updated at the same time.
The internal upkeep_oracles method is responsible for updating the price and D oracle.
Info
Both EMA values, ema_price and ma_D, are updated maximally once per block. If there are two or more actions within the same block that would update the oracles, only the first action will update these values. The spot price (last_price or last_D) will always update.
The rationale behind this approach is that all transactions within a block share the same timestamp. Therefore, the condition if ma_last_time < block.timestamp can only be satisfied once per block (the first time it's called). If there are multiple actions that would trigger an oracle update, it will be updated in the next relevant action.
Source code for the internal upkeep_oracle function
@internaldefupkeep_oracles(xp:DynArray[uint256,MAX_COINS],amp:uint256,D:uint256):""" @notice Upkeeps price and D oracles. """ma_last_time_unpacked:uint256[2]=self.unpack_2(self.ma_last_time)last_prices_packed_current:DynArray[uint256,MAX_COINS]=self.last_prices_packedlast_prices_packed_new:DynArray[uint256,MAX_COINS]=last_prices_packed_currentspot_price:DynArray[uint256,MAX_COINS]=self._get_p(xp,amp,D)# -------------------------- Upkeep price oracle -------------------------foriinrange(MAX_COINS):ifi==N_COINS-1:breakifspot_price[i]!=0:# Update packed prices -----------------last_prices_packed_new[i]=self.pack_2(min(spot_price[i],2*10**18),# <----- Cap spot value by 2.self._calc_moving_average(last_prices_packed_current[i],self.ma_exp_time,ma_last_time_unpacked[0],# index 0 is ma_last_time for prices))self.last_prices_packed=last_prices_packed_new# ---------------------------- Upkeep D oracle ---------------------------last_D_packed_current:uint256=self.last_D_packedself.last_D_packed=self.pack_2(D,self._calc_moving_average(last_D_packed_current,self.D_ma_time,ma_last_time_unpacked[1],# index 1 is ma_last_time for D))# Housekeeping: Update ma_last_time for p and D oracles ------------------foriinrange(2):ifma_last_time_unpacked[i]<block.timestamp:ma_last_time_unpacked[i]=block.timestampself.ma_last_time=self.pack_2(ma_last_time_unpacked[0],ma_last_time_unpacked[1])
Liquidity removal in an imbalanced proportion (remove_liquidity_imbalance)
When price oracles are upkept, the code calculates both the spot price and the moving-average price. These values are then packed and stored together in last_prices_packed.
# -------------------------- Upkeep price oracle -------------------------foriinrange(MAX_COINS):ifi==N_COINS-1:breakifspot_price[i]!=0:# Update packed prices -----------------last_prices_packed_new[i]=self.pack_2(min(spot_price[i],2*10**18),# <----- Cap spot value by 2.self._calc_moving_average(last_prices_packed_current[i],self.ma_exp_time,ma_last_time_unpacked[0],# index 0 is ma_last_time for prices))self.last_prices_packed=last_prices_packed_new
last_price which represents the last stored spot price within the AMM is calculated using _get_p. Additionally, the value is capped at 2 * 10**18 to prevent price oracle manipulation. Note: It's not actually the spot price which is capped, but rather the spot price that is used in the calculation for the EMA price oracle.
_get_p
@internal@puredef_get_p(xp:DynArray[uint256,MAX_COINS],amp:uint256,D:uint256,)->DynArray[uint256,MAX_COINS]:# dx_0 / dx_1 only, however can have any number of coins in poolANN:uint256=unsafe_mul(amp,N_COINS)Dr:uint256=unsafe_div(D,pow_mod256(N_COINS,N_COINS))foriinrange(N_COINS_128,bound=MAX_COINS_128):Dr=Dr*D/xp[i]p:DynArray[uint256,MAX_COINS]=empty(DynArray[uint256,MAX_COINS])xp0_A:uint256=unsafe_div(ANN*xp[0],A_PRECISION)foriinrange(1,MAX_COINS):ifi==N_COINS:breakp.append(10**18*(xp0_A+unsafe_div(Dr*xp[0],xp[i]))/(xp0_A+Dr))returnp
The moving-average price (ema_price) is calculated using _calc_moving_average. This value can only be updated once per block. If there are two actions which would update the value, only the first action will update it. For the second action, only the last_price is updated, while ema_price will not be updated and have the same value as in the first action.
_calc_moving_average
@internal@viewdef_calc_moving_average(packed_value:uint256,averaging_window:uint256,ma_last_time:uint256)->uint256:last_spot_value:uint256=packed_value&(2**128-1)last_ema_value:uint256=(packed_value>>128)ifma_last_time<block.timestamp:# calculate new_ema_value and return that.alpha:uint256=self.exp(-convert(unsafe_div(unsafe_mul(unsafe_sub(block.timestamp,ma_last_time),10**18),averaging_window),int256))returnunsafe_div(last_spot_value*(10**18-alpha)+last_ema_value*alpha,10**18)returnlast_ema_value
Liquidity removal in an imbalanced proportion (remove_liquidity_imbalance)
Balanced proportion liquidity removal. For this action, the remove_liquidity function, which executes it, does not directly call the upkeep_oracles method. Instead, the D oracle update is performed "manually" within the function. The rationale behind this approach is that updating the price oracle is not necessary in this scenario, because removing in a balanced proportion does not change the prices within the AMM.
When the D oracle is updated, the code calculates both the "spot" D invariant and the moving-average D invariant value. These values are then packed and stored together in last_D_packed.
# ---------------------------- Upkeep D oracle ---------------------------last_D_packed_current:uint256=self.last_D_packedself.last_D_packed=self.pack_2(D,self._calc_moving_average(last_D_packed_current,self.D_ma_time,ma_last_time_unpacked[1],# index 1 is ma_last_time for D))