Price Aggregator
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.
This aggregated price of crvUSD is used in multiple different components in the system such as in monetary policy contracts, PegKeepers or oracles for lending markets.
Calculations¶
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.
EMA TVL Calculation¶
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
@view
def _ema_tvl() -> DynArray[uint256, MAX_PAIRS]:
tvls: DynArray[uint256, MAX_PAIRS] = []
last_timestamp: uint256 = self.last_timestamp
alpha: uint256 = 10**18
if last_timestamp < block.timestamp:
alpha = self.exp(- convert((block.timestamp - last_timestamp) * 10**18 / TVL_MA_TIME, int256))
n_price_pairs: uint256 = self.n_price_pairs
for i in range(MAX_PAIRS):
if i == n_price_pairs:
break
tvl: uint256 = self.last_tvl[i]
if alpha != 10**18:
# alpha = 1.0 when dt = 0
# alpha = 0.0 when dt = inf
new_tvl: uint256 = self.price_pairs[i].pool.totalSupply() # We don't do virtual price here to save on gas
tvl = (new_tvl * (10**18 - alpha) + tvl * alpha) / 10**18
tvls.append(tvl)
return tvls
Aggregated crvUSD Price Calculation¶
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
@view
def _price(tvls: DynArray[uint256, MAX_PAIRS]) -> uint256:
n: uint256 = self.n_price_pairs
prices: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
D: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
Dsum: uint256 = 0
DPsum: uint256 = 0
for i in range(MAX_PAIRS):
if i == n:
break
price_pair: PricePair = self.price_pairs[i]
pool_supply: uint256 = tvls[i]
if pool_supply >= MIN_LIQUIDITY:
p: uint256 = price_pair.pool.price_oracle()
if price_pair.is_inverse:
p = 10**36 / p
prices[i] = p
D[i] = pool_supply
Dsum += pool_supply
DPsum += pool_supply * p
if Dsum == 0:
return 10**18 # Placeholder for no active pools
p_avg: uint256 = DPsum / Dsum
e: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
e_min: uint256 = max_value(uint256)
for i in range(MAX_PAIRS):
if i == n:
break
p: 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 = 0
w_sum: uint256 = 0
for i in range(MAX_PAIRS):
if i == n:
break
w: uint256 = D[i] * self.exp(-convert(e[i] - e_min, int256)) / 10**18
w_sum += w
wp_sum += w * prices[i]
return wp_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 aprices[i]
array for each pool with enough liquidity. - Storing each pool's TVL in
D[i]
, adding this TVL toDsum
, and summing up the product of thecrvUSD
price and pool supply inDPsum
.
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:
Price and TVL Methods¶
price
¶
PriceAggregator3.price() -> uint256
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) = 20
MIN_LIQUIDITY: constant(uint256) = 100_000 * 10**18 # Only take into account pools with enough liquidity
STABLECOIN: immutable(address)
SIGMA: immutable(uint256)
price_pairs: public(PricePair[MAX_PAIRS])
n_price_pairs: uint256
last_timestamp: public(uint256)
last_tvl: public(uint256[MAX_PAIRS])
TVL_MA_TIME: public(constant(uint256)) = 50000 # s
last_price: public(uint256)
@external
@view
def price() -> uint256:
return self._price(self._ema_tvl())
@internal
@view
def _price(tvls: DynArray[uint256, MAX_PAIRS]) -> uint256:
n: uint256 = self.n_price_pairs
prices: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
D: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
Dsum: uint256 = 0
DPsum: uint256 = 0
for i in range(MAX_PAIRS):
if i == n:
break
price_pair: PricePair = self.price_pairs[i]
pool_supply: uint256 = tvls[i]
if pool_supply >= MIN_LIQUIDITY:
p: uint256 = 0
if price_pair.include_index:
p = price_pair.pool.price_oracle(0)
else:
p = price_pair.pool.price_oracle()
if price_pair.is_inverse:
p = 10**36 / p
prices[i] = p
D[i] = pool_supply
Dsum += pool_supply
DPsum += pool_supply * p
if Dsum == 0:
return 10**18 # Placeholder for no active pools
p_avg: uint256 = DPsum / Dsum
e: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
e_min: uint256 = max_value(uint256)
for i in range(MAX_PAIRS):
if i == n:
break
p: 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 = 0
w_sum: uint256 = 0
for i in range(MAX_PAIRS):
if i == n:
break
w: uint256 = D[i] * self.exp(-convert(e[i] - e_min, int256)) / 10**18
w_sum += w
wp_sum += w * prices[i]
return wp_sum / w_sum
@internal
@view
def _ema_tvl() -> DynArray[uint256, MAX_PAIRS]:
tvls: DynArray[uint256, MAX_PAIRS] = []
last_timestamp: uint256 = self.last_timestamp
alpha: uint256 = 10**18
if last_timestamp < block.timestamp:
alpha = self.exp(- convert((block.timestamp - last_timestamp) * 10**18 / TVL_MA_TIME, int256))
n_price_pairs: uint256 = self.n_price_pairs
for i in range(MAX_PAIRS):
if i == n_price_pairs:
break
tvl: uint256 = self.last_tvl[i]
if alpha != 10**18:
# alpha = 1.0 when dt = 0
# alpha = 0.0 when dt = inf
new_tvl: uint256 = self.price_pairs[i].pool.totalSupply() # We don't do virtual price here to save on gas
tvl = (new_tvl * (10**18 - alpha) + tvl * alpha) / 10**18
tvls.append(tvl)
return tvls
price_w
¶
PriceAggregator3.price_w() -> uint256
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) = 20
MIN_LIQUIDITY: constant(uint256) = 100_000 * 10**18 # Only take into account pools with enough liquidity
STABLECOIN: immutable(address)
SIGMA: immutable(uint256)
price_pairs: public(PricePair[MAX_PAIRS])
n_price_pairs: uint256
last_timestamp: public(uint256)
last_tvl: public(uint256[MAX_PAIRS])
TVL_MA_TIME: public(constant(uint256)) = 50000 # s
last_price: public(uint256)
@external
def price_w() -> uint256:
if self.last_timestamp == block.timestamp:
return self.last_price
else:
ema_tvl: DynArray[uint256, MAX_PAIRS] = self._ema_tvl()
self.last_timestamp = block.timestamp
for i in range(MAX_PAIRS):
if i == len(ema_tvl):
break
self.last_tvl[i] = ema_tvl[i]
p: uint256 = self._price(ema_tvl)
self.last_price = p
return p
@internal
@view
def _price(tvls: DynArray[uint256, MAX_PAIRS]) -> uint256:
n: uint256 = self.n_price_pairs
prices: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
D: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
Dsum: uint256 = 0
DPsum: uint256 = 0
for i in range(MAX_PAIRS):
if i == n:
break
price_pair: PricePair = self.price_pairs[i]
pool_supply: uint256 = tvls[i]
if pool_supply >= MIN_LIQUIDITY:
p: uint256 = 0
if price_pair.include_index:
p = price_pair.pool.price_oracle(0)
else:
p = price_pair.pool.price_oracle()
if price_pair.is_inverse:
p = 10**36 / p
prices[i] = p
D[i] = pool_supply
Dsum += pool_supply
DPsum += pool_supply * p
if Dsum == 0:
return 10**18 # Placeholder for no active pools
p_avg: uint256 = DPsum / Dsum
e: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
e_min: uint256 = max_value(uint256)
for i in range(MAX_PAIRS):
if i == n:
break
p: 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 = 0
w_sum: uint256 = 0
for i in range(MAX_PAIRS):
if i == n:
break
w: uint256 = D[i] * self.exp(-convert(e[i] - e_min, int256)) / 10**18
w_sum += w
wp_sum += w * prices[i]
return wp_sum / w_sum
@internal
@view
def _ema_tvl() -> DynArray[uint256, MAX_PAIRS]:
tvls: DynArray[uint256, MAX_PAIRS] = []
last_timestamp: uint256 = self.last_timestamp
alpha: uint256 = 10**18
if last_timestamp < block.timestamp:
alpha = self.exp(- convert((block.timestamp - last_timestamp) * 10**18 / TVL_MA_TIME, int256))
n_price_pairs: uint256 = self.n_price_pairs
for i in range(MAX_PAIRS):
if i == n_price_pairs:
break
tvl: uint256 = self.last_tvl[i]
if alpha != 10**18:
# alpha = 1.0 when dt = 0
# alpha = 0.0 when dt = inf
new_tvl: uint256 = self.price_pairs[i].pool.totalSupply() # We don't do virtual price here to save on gas
tvl = (new_tvl * (10**18 - alpha) + tvl * alpha) / 10**18
tvls.append(tvl)
return tvls
last_price
¶
PriceAggregator3.last_price() -> uint256: view
Getter for the last aggregated price of crvUSD. This variable was set to \(10^{18}\) (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_timestamp
¶
PriceAggregator3.last_timestamp() -> uint256: view
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.
ema_tvl
¶
PriceAggregator3.ema_tvl() -> DynArray[uint256, MAX_PAIRS]
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) = 20
MIN_LIQUIDITY: constant(uint256) = 100_000 * 10**18 # Only take into account pools with enough liquidity
price_pairs: public(PricePair[MAX_PAIRS])
n_price_pairs: uint256
last_timestamp: public(uint256)
last_tvl: public(uint256[MAX_PAIRS])
TVL_MA_TIME: public(constant(uint256)) = 50000 # s
@external
@view
def ema_tvl() -> DynArray[uint256, MAX_PAIRS]:
return self._ema_tvl()
@internal
@view
def _ema_tvl() -> DynArray[uint256, MAX_PAIRS]:
tvls: DynArray[uint256, MAX_PAIRS] = []
last_timestamp: uint256 = self.last_timestamp
alpha: uint256 = 10**18
if last_timestamp < block.timestamp:
alpha = self.exp(- convert((block.timestamp - last_timestamp) * 10**18 / TVL_MA_TIME, int256))
n_price_pairs: uint256 = self.n_price_pairs
for i in range(MAX_PAIRS):
if i == n_price_pairs:
break
tvl: uint256 = self.last_tvl[i]
if alpha != 10**18:
# alpha = 1.0 when dt = 0
# alpha = 0.0 when dt = inf
new_tvl: uint256 = self.price_pairs[i].pool.totalSupply() # We don't do virtual price here to save on gas
tvl = (new_tvl * (10**18 - alpha) + tvl * alpha) / 10**18
tvls.append(tvl)
return tvls
last_tvl
¶
PriceAggregator3.last_tvl(arg0: uint256) -> uint256: view
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.
TVL_MA_TIME
¶
PriceAggregator3.TVL_MA_TIME() -> uint256: view
Getter for the time periodicity used to calculate the exponential moving-average of TVL.
Returns: ema periodicity (uint256
).
Source code
The following source code includes all changes up to commit hash 86cae3a; any changes made after this commit are not included.
Contract Info Methods¶
sigma
¶
PriceAggregator3.SIGMA() -> uint256: view
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.
stablecoin
¶
PriceAggregator3.STABLECOIN() -> uint256: view
Getter for the crvUSD contract address.
Returns: crvUSD contract (address
).
Source code
The following source code includes all changes up to commit hash 86cae3a; any changes made after this commit are not included.
Price Pairs¶
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
.
price_pairs
¶
PriceAggregator3.price_pairs(arg0: uint256) -> PricePair
Getter for the price pairs added to the PriceAggregator
contract. New pairs can be added using the add_price_pair
function.
Returns: PricePair
struct consisting of the pool (address
) amd of it is inverse (bool
).
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.
=== PriceAggregator3.vy"
```python
struct PricePair:
pool: Stableswap
is_inverse: bool
include_index: bool
price_pairs: public(PricePair[MAX_PAIRS])
```
add_price_pair
¶
PriceAggregator3.add_price_pair(_pool: Stableswap)
Guarded Method
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.
event AddPricePair:
n: uint256
pool: Stableswap
is_inverse: bool
price_pairs: public(PricePair[MAX_PAIRS])
n_price_pairs: uint256
@external
def add_price_pair(_pool: Stableswap):
assert msg.sender == self.admin
price_pair: PricePair = empty(PricePair)
price_pair.pool = _pool
coins: address[2] = [_pool.coins(0), _pool.coins(1)]
if coins[0] == STABLECOIN:
price_pair.is_inverse = True
else:
assert coins[1] == STABLECOIN
n: uint256 = self.n_price_pairs
self.price_pairs[n] = price_pair # Should revert if too many pairs
self.last_tvl[n] = _pool.totalSupply()
self.n_price_pairs = n + 1
log AddPricePair(n, _pool, price_pair.is_inverse)
remove_price_pair
¶
PriceAggregator3.remove_price_pair(n: uint256)
Guarded Method
This function is only callable by the admin
of the contract.
Function to remove the price pair at index n
from the PriceAggregator
.
Emits: RemovePricePair
and conditionally MovePricePair
1.
Input | Type | Description |
---|---|---|
n | uint256 | Index of the price pair to remove |
Source code
The following source code includes all changes up to commit hash 86cae3a; any changes made after this commit are not included.
event RemovePricePair:
n: uint256
event MovePricePair:
n_from: uint256
n_to: uint256
price_pairs: public(PricePair[MAX_PAIRS])
n_price_pairs: uint256
@external
def remove_price_pair(n: uint256):
assert msg.sender == self.admin
n_max: uint256 = self.n_price_pairs - 1
assert n <= n_max
if n < n_max:
self.price_pairs[n] = self.price_pairs[n_max]
log MovePricePair(n_max, n)
self.n_price_pairs = n_max
log RemovePricePair(n)
Contract Ownership¶
The contract follows the classical two-step ownership model used in various other Curve contracts:
admin
¶
PriceAggregator3.admin() -> address: view
Getter for the current admin of the contract.
Returns: current admin (address
).
Source code
The following source code includes all changes up to commit hash 86cae3a; any changes made after this commit are not included.
set_admin
¶
PriceAggregator3.set_admin(_admin: address)
Guarded Method
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.
-
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. ↩