The AggregateStablePrice.vy contract is designed to get an aggregated price of crvUSD based on multiple multiple stableswap pools weighted by their TVL.
GitHub
There are three iterations of the AggregateStablePrice contract. Source code for the contracts can be found on GitHub.
The AggregateStablePrice.vy contract has been deployed on Ethereum and Arbitrum.
The AggregateStablePrice contract calculates the weighted average price of crvUSD across multiple liquidity pools, considering only those pools with sufficient liquidity (MIN_LIQUIDITY = 100,000 * 10**18). The calculation is based on the exponential moving average (EMA) of the Total-Value-Locked (TVL) for each pool, determining the liquidity considered in the price aggregation.
The price calculation starts with determining the EMA of the TVL from different Curve Stableswap liquidity pools using the _ema_tvl function. This internal function computes the EMA TVLs based on the formula below, which adjusts for the time since the last update to smooth out short-term volatility in the TVL data, providing a more stable and representative average value over the specified time window (TVL_MA_TIME = 50000):
The code snippet provided illustrates the implementation of the above formula in the contract.
Source code for _ema_tvl
TVL_MA_TIME:public(constant(uint256))=50000# s@internal@viewdef_ema_tvl()->DynArray[uint256,MAX_PAIRS]:tvls:DynArray[uint256,MAX_PAIRS]=[]last_timestamp:uint256=self.last_timestampalpha:uint256=10**18iflast_timestamp<block.timestamp:alpha=self.exp(-convert((block.timestamp-last_timestamp)*10**18/TVL_MA_TIME,int256))n_price_pairs:uint256=self.n_price_pairsforiinrange(MAX_PAIRS):ifi==n_price_pairs:breaktvl:uint256=self.last_tvl[i]ifalpha!=10**18:# alpha = 1.0 when dt = 0# alpha = 0.0 when dt = infnew_tvl:uint256=self.price_pairs[i].pool.totalSupply()# We don't do virtual price here to save on gastvl=(new_tvl*(10**18-alpha)+tvl*alpha)/10**18tvls.append(tvl)returntvls
The _price function then uses these EMA TVLs to calculate the aggregated price of crvUSD by considering the liquidity of each pool. The function adjusts the price from the pool's price_oracle based on the coin index of crvUSD in the liquidity pool.
Source code for _price
@internal@viewdef_price(tvls:DynArray[uint256,MAX_PAIRS])->uint256:n:uint256=self.n_price_pairsprices:uint256[MAX_PAIRS]=empty(uint256[MAX_PAIRS])D:uint256[MAX_PAIRS]=empty(uint256[MAX_PAIRS])Dsum:uint256=0DPsum:uint256=0foriinrange(MAX_PAIRS):ifi==n:breakprice_pair:PricePair=self.price_pairs[i]pool_supply:uint256=tvls[i]ifpool_supply>=MIN_LIQUIDITY:p:uint256=price_pair.pool.price_oracle()ifprice_pair.is_inverse:p=10**36/pprices[i]=pD[i]=pool_supplyDsum+=pool_supplyDPsum+=pool_supply*pifDsum==0:return10**18# Placeholder for no active poolsp_avg:uint256=DPsum/Dsume:uint256[MAX_PAIRS]=empty(uint256[MAX_PAIRS])e_min:uint256=max_value(uint256)foriinrange(MAX_PAIRS):ifi==n:breakp:uint256=prices[i]e[i]=(max(p,p_avg)-min(p,p_avg))**2/(SIGMA**2/10**18)e_min=min(e[i],e_min)wp_sum:uint256=0w_sum:uint256=0foriinrange(MAX_PAIRS):ifi==n:breakw:uint256=D[i]*self.exp(-convert(e[i]-e_min,int256))/10**18w_sum+=wwp_sum+=w*prices[i]returnwp_sum/w_sum
In the calculation process, the contract iterates over all price pairs to perform the following steps:
Storing the price of crvUSD in a prices[i] array for each pool with enough liquidity.
Storing each pool's TVL in D[i], adding this TVL to Dsum, and summing up the product of the crvUSD price and pool supply in DPsum.
Finally, the contract calculates an average price:
Next, a variance measure e is computed for each pool's price relative to the average, adjusting by SIGMA to normalize:
Applying an exponential decay based on these variance measures to weigh each pool's contribution to the final average price, reducing the influence of prices far from the minimum variance.
Next, sum up all w to store it in w_sum and calculate the product of w * prices[i], which is stored in wp_sum.
Finally, the weighted average price of crvUSD is calculated:
Getter for the aggregated price of crvUSD based on the prices of crvUSD within different price_pairs.
Returns: aggregated crvUSD price (uint256).
Source code
The following source code includes all changes up to commit hash 86cae3a; any changes made after this commit are not included.
MAX_PAIRS:constant(uint256)=20MIN_LIQUIDITY:constant(uint256)=100_000*10**18# Only take into account pools with enough liquiditySTABLECOIN:immutable(address)SIGMA:immutable(uint256)price_pairs:public(PricePair[MAX_PAIRS])n_price_pairs:uint256last_timestamp:public(uint256)last_tvl:public(uint256[MAX_PAIRS])TVL_MA_TIME:public(constant(uint256))=50000# slast_price:public(uint256)@external@viewdefprice()->uint256:returnself._price(self._ema_tvl())@internal@viewdef_price(tvls:DynArray[uint256,MAX_PAIRS])->uint256:n:uint256=self.n_price_pairsprices:uint256[MAX_PAIRS]=empty(uint256[MAX_PAIRS])D:uint256[MAX_PAIRS]=empty(uint256[MAX_PAIRS])Dsum:uint256=0DPsum:uint256=0foriinrange(MAX_PAIRS):ifi==n:breakprice_pair:PricePair=self.price_pairs[i]pool_supply:uint256=tvls[i]ifpool_supply>=MIN_LIQUIDITY:p:uint256=0ifprice_pair.include_index:p=price_pair.pool.price_oracle(0)else:p=price_pair.pool.price_oracle()ifprice_pair.is_inverse:p=10**36/pprices[i]=pD[i]=pool_supplyDsum+=pool_supplyDPsum+=pool_supply*pifDsum==0:return10**18# Placeholder for no active poolsp_avg:uint256=DPsum/Dsume:uint256[MAX_PAIRS]=empty(uint256[MAX_PAIRS])e_min:uint256=max_value(uint256)foriinrange(MAX_PAIRS):ifi==n:breakp:uint256=prices[i]e[i]=(max(p,p_avg)-min(p,p_avg))**2/(SIGMA**2/10**18)e_min=min(e[i],e_min)wp_sum:uint256=0w_sum:uint256=0foriinrange(MAX_PAIRS):ifi==n:breakw:uint256=D[i]*self.exp(-convert(e[i]-e_min,int256))/10**18w_sum+=wwp_sum+=w*prices[i]returnwp_sum/w_sum@internal@viewdef_ema_tvl()->DynArray[uint256,MAX_PAIRS]:tvls:DynArray[uint256,MAX_PAIRS]=[]last_timestamp:uint256=self.last_timestampalpha:uint256=10**18iflast_timestamp<block.timestamp:alpha=self.exp(-convert((block.timestamp-last_timestamp)*10**18/TVL_MA_TIME,int256))n_price_pairs:uint256=self.n_price_pairsforiinrange(MAX_PAIRS):ifi==n_price_pairs:breaktvl:uint256=self.last_tvl[i]ifalpha!=10**18:# alpha = 1.0 when dt = 0# alpha = 0.0 when dt = infnew_tvl:uint256=self.price_pairs[i].pool.totalSupply()# We don't do virtual price here to save on gastvl=(new_tvl*(10**18-alpha)+tvl*alpha)/10**18tvls.append(tvl)returntvls
Function to calculate the aggregated price of crvUSD based on the prices of crvUSD within different price_pairs. This function writes the price on the blockchain and additionally updates last_timestamp, last_tvl and last_price.
Returns: aggregated crvUSD price (uint256).
Source code
The following source code includes all changes up to commit hash 86cae3a; any changes made after this commit are not included.
MAX_PAIRS:constant(uint256)=20MIN_LIQUIDITY:constant(uint256)=100_000*10**18# Only take into account pools with enough liquiditySTABLECOIN:immutable(address)SIGMA:immutable(uint256)price_pairs:public(PricePair[MAX_PAIRS])n_price_pairs:uint256last_timestamp:public(uint256)last_tvl:public(uint256[MAX_PAIRS])TVL_MA_TIME:public(constant(uint256))=50000# slast_price:public(uint256)@externaldefprice_w()->uint256:ifself.last_timestamp==block.timestamp:returnself.last_priceelse:ema_tvl:DynArray[uint256,MAX_PAIRS]=self._ema_tvl()self.last_timestamp=block.timestampforiinrange(MAX_PAIRS):ifi==len(ema_tvl):breakself.last_tvl[i]=ema_tvl[i]p:uint256=self._price(ema_tvl)self.last_price=preturnp@internal@viewdef_price(tvls:DynArray[uint256,MAX_PAIRS])->uint256:n:uint256=self.n_price_pairsprices:uint256[MAX_PAIRS]=empty(uint256[MAX_PAIRS])D:uint256[MAX_PAIRS]=empty(uint256[MAX_PAIRS])Dsum:uint256=0DPsum:uint256=0foriinrange(MAX_PAIRS):ifi==n:breakprice_pair:PricePair=self.price_pairs[i]pool_supply:uint256=tvls[i]ifpool_supply>=MIN_LIQUIDITY:p:uint256=0ifprice_pair.include_index:p=price_pair.pool.price_oracle(0)else:p=price_pair.pool.price_oracle()ifprice_pair.is_inverse:p=10**36/pprices[i]=pD[i]=pool_supplyDsum+=pool_supplyDPsum+=pool_supply*pifDsum==0:return10**18# Placeholder for no active poolsp_avg:uint256=DPsum/Dsume:uint256[MAX_PAIRS]=empty(uint256[MAX_PAIRS])e_min:uint256=max_value(uint256)foriinrange(MAX_PAIRS):ifi==n:breakp:uint256=prices[i]e[i]=(max(p,p_avg)-min(p,p_avg))**2/(SIGMA**2/10**18)e_min=min(e[i],e_min)wp_sum:uint256=0w_sum:uint256=0foriinrange(MAX_PAIRS):ifi==n:breakw:uint256=D[i]*self.exp(-convert(e[i]-e_min,int256))/10**18w_sum+=wwp_sum+=w*prices[i]returnwp_sum/w_sum@internal@viewdef_ema_tvl()->DynArray[uint256,MAX_PAIRS]:tvls:DynArray[uint256,MAX_PAIRS]=[]last_timestamp:uint256=self.last_timestampalpha:uint256=10**18iflast_timestamp<block.timestamp:alpha=self.exp(-convert((block.timestamp-last_timestamp)*10**18/TVL_MA_TIME,int256))n_price_pairs:uint256=self.n_price_pairsforiinrange(MAX_PAIRS):ifi==n_price_pairs:breaktvl:uint256=self.last_tvl[i]ifalpha!=10**18:# alpha = 1.0 when dt = 0# alpha = 0.0 when dt = infnew_tvl:uint256=self.price_pairs[i].pool.totalSupply()# We don't do virtual price here to save on gastvl=(new_tvl*(10**18-alpha)+tvl*alpha)/10**18tvls.append(tvl)returntvls
Getter for the last aggregated price of crvUSD. This variable was set to (1.00) when initializing the contract and is updated to the current aggreagated crvUSD price every time price_w is called.
Returns: last aggregated price of crvUSD (uint256).
Source code
The following source code includes all changes up to commit hash 86cae3a; any changes made after this commit are not included.
last_price:public(uint256)@externaldef__init__(stablecoin:address,sigma:uint256,admin:address):STABLECOIN=stablecoinSIGMA=sigma# The change is so rare that we can change the whole thing altogetherself.admin=adminself.last_price=10**18self.last_timestamp=block.timestamp
Getter for the last timestamp when the aggregated price of crvUSD was updated. This variable was populated with block.timestamp when initializing the contract and is updated to the current timestamp every time price_w is called. When adding a new price pair, its value is set to the totalSupply of the pair.
Returns: timestamp of the last price write (uint256).
Source code
The following source code includes all changes up to commit hash 86cae3a; any changes made after this commit are not included.
Getter for the exponential moving-average value of TVL across all price_pairs.
Returns: array of ema tvls (DynArray[uint256, MAX_PAIRS]).
Source code
The following source code includes all changes up to commit hash 86cae3a; any changes made after this commit are not included.
MAX_PAIRS:constant(uint256)=20MIN_LIQUIDITY:constant(uint256)=100_000*10**18# Only take into account pools with enough liquidityprice_pairs:public(PricePair[MAX_PAIRS])n_price_pairs:uint256last_timestamp:public(uint256)last_tvl:public(uint256[MAX_PAIRS])TVL_MA_TIME:public(constant(uint256))=50000# s@external@viewdefema_tvl()->DynArray[uint256,MAX_PAIRS]:returnself._ema_tvl()@internal@viewdef_ema_tvl()->DynArray[uint256,MAX_PAIRS]:tvls:DynArray[uint256,MAX_PAIRS]=[]last_timestamp:uint256=self.last_timestampalpha:uint256=10**18iflast_timestamp<block.timestamp:alpha=self.exp(-convert((block.timestamp-last_timestamp)*10**18/TVL_MA_TIME,int256))n_price_pairs:uint256=self.n_price_pairsforiinrange(MAX_PAIRS):ifi==n_price_pairs:breaktvl:uint256=self.last_tvl[i]ifalpha!=10**18:# alpha = 1.0 when dt = 0# alpha = 0.0 when dt = infnew_tvl:uint256=self.price_pairs[i].pool.totalSupply()# We don't do virtual price here to save on gastvl=(new_tvl*(10**18-alpha)+tvl*alpha)/10**18tvls.append(tvl)returntvls
Getter for the last ema tvl value of a price_pair. This variable is updated to the current ema tvl of the pool every time price_w is called. When adding a new price pair, its value is set to the totalSupply of the pair.
Returns: last ema tvl (uint256).
Input
Type
Description
arg0
uint256
Index of the price pair
Source code
The following source code includes all changes up to commit hash 86cae3a; any changes made after this commit are not included.
Getter for the sigma value. SIGMA is a predefined constant that influences the adjustment of price deviations, affecting how variations in individual stablecoin prices contribute to the overall average stablecoin price. The value of sigma was set to 1000000000000000 when initializing the contract and the variable is immutale, meaning it can not be adjusted.
Returns: sigma value (uint256).
Source code
The following source code includes all changes up to commit hash 86cae3a; any changes made after this commit are not included.
SIGMA:immutable(uint256)@externaldef__init__(stablecoin:address,sigma:uint256,admin:address):STABLECOIN=stablecoinSIGMA=sigma# The change is so rare that we can change the whole thing altogetherself.admin=adminself.last_price=10**18self.last_timestamp=block.timestamp
The following source code includes all changes up to commit hash 86cae3a; any changes made after this commit are not included.
STABLECOIN:immutable(address)@externaldef__init__(stablecoin:address,sigma:uint256,admin:address):STABLECOIN=stablecoinSIGMA=sigma# The change is so rare that we can change the whole thing altogetherself.admin=adminself.last_price=10**18self.last_timestamp=block.timestamp
All liquidity pools used to calculate the aggregated price are stored in price_pairs. New price pairs can be added or removed by the DAO using add_price_pair and remove_price_pair.
>>>PriceAggregator3.price_pairs(0)# PriceAggregator on Ethereum'0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E, false'>>>PriceAggregator3.price_pairs(0)# PriceAggregator on Arbitrum'0xec090cf6DD891D2d014beA6edAda6e05E025D93d, true, true'
This function is only callable by the admin of the contract.
Function to add a new price pair to the PriceAggregator.
Emits: AddPricePair
Input
Type
Description
_pool
address
Pool to add as price pair
Source code
The following source code includes all changes up to commit hash 86cae3a; any changes made after this commit are not included.
eventAddPricePair:n:uint256pool:Stableswapis_inverse:boolprice_pairs:public(PricePair[MAX_PAIRS])n_price_pairs:uint256@externaldefadd_price_pair(_pool:Stableswap):assertmsg.sender==self.adminprice_pair:PricePair=empty(PricePair)price_pair.pool=_poolcoins:address[2]=[_pool.coins(0),_pool.coins(1)]ifcoins[0]==STABLECOIN:price_pair.is_inverse=Trueelse:assertcoins[1]==STABLECOINn:uint256=self.n_price_pairsself.price_pairs[n]=price_pair# Should revert if too many pairsself.last_tvl[n]=_pool.totalSupply()self.n_price_pairs=n+1logAddPricePair(n,_pool,price_pair.is_inverse)
The following source code includes all changes up to commit hash 86cae3a; any changes made after this commit are not included.
admin:public(address)@externaldef__init__(stablecoin:address,sigma:uint256,admin:address):STABLECOIN=stablecoinSIGMA=sigma# The change is so rare that we can change the whole thing altogetherself.admin=adminself.last_price=10**18self.last_timestamp=block.timestamp
This function is only callable by the admin of the contract.
Function to set a new adderss as the admin of the contract.
Emits: SetAdmin
Input
Type
Description
_admin
uint256
New address to set the admin to
Source code
The following source code includes all changes up to commit hash 86cae3a; any changes made after this commit are not included.
eventSetAdmin:admin:addressadmin:public(address)@externaldefset_admin(_admin:address):# We are not doing commit / apply because the owner will be a voting DAO anyway# which has vote delaysassertmsg.sender==self.adminself.admin=_adminlogSetAdmin(_admin)
>>>soon
MovePricePair event is emitted when the removed price pair is not the last one which was added. In this case, price pairs need to be adjusted accordingly. ↩