PriceAggregator
The AggregatorStablePrice contract is designed to aggregate the prices of crvUSD based on multiple Curve Stableswap pools. This price is primarily used as an oracle for calculating the interest rate, but also for PegKeepers to determine whether to mint and deposit or withdraw and burn.
Contract Source & Deployment
AggregatorStablePrice contract is deployed to the Ethereum mainnet at: 0x18672b1b0c623a30089A280Ed9256379fb0E4E62. Source code available on Github.
Exponential Moving Average of TVL¶
The internal _ema_tvl()
calculates the Exponential Moving Average (EMA) of the Total Value Locked (TVL) for multiple Curve StableSwap pools. There is a maximum of 20 pairs to consider, and each price pair (pool) must have at least 100k TVL.
New pairs can be added via add_price_pair
.
Source code
last_tvl: public(uint256[MAX_PAIRS])
@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
ema_tvl
¶
PriceAggregator.ema_tvl() -> DynArray[uint256, MAX_PAIRS]
Getter for the exponential moving average of the TVL in price_pairs
.
Returns: array of ema tvls (DynArray[uint256, MAX_PAIRS]
).
Source code
TVL_MA_TIME: public(constant(uint256)) = 50000 # s
last_tvl: public(uint256[MAX_PAIRS])
@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
price
¶
PriceAggregator.price() -> uint256:
Function to calculate the weighted price of crvUSD.
Returns: price (uint256
).
Source code
interface Stableswap:
def price_oracle() -> uint256: view
def coins(i: uint256) -> address: view
def get_virtual_price() -> uint256: view
def totalSupply() -> uint256: view
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
@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 = 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
Adding and Removing Price Pairs¶
price_pairs
¶
PriceAggregator.price_pairs(arg0: uint256) -> tuple: view
Getter for the price pair at index arg0
and whether the price pair is inverse.
Returns: price pair (address
) and true or false (bool
).
Input | Type | Description |
---|---|---|
arg0 | uint256 | Index of the price pair |
add_price_pair
¶
PriceAggregator.add_price_pair(_pool: Stableswap):
Guarded Method
This function is only callable by the admin
of the contract.
Function to add a price pair to the PriceAggregator.
Emits: AddPricePair
Input | Type | Description |
---|---|---|
_pool | address | Price pair to add |
Source code
event AddPricePair:
n: uint256
pool: Stableswap
is_inverse: bool
@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
¶
PriceAggregator.remove_price_pair(n: uint256):
Guarded Method
This function is only callable by the admin
of the contract.
Function to remove a price pair from the contract. If a prior pool than the latest added one gets removed, the function will move the latest added price pair to the removed pair pairs index to not mess up price_pairs
.
Emits: RemovePricePair
and possibly MovePricePair
Input | Type | Description |
---|---|---|
n | uint256 | Index of the price pair to remove |
Source code
event RemovePricePair:
n: uint256
event MovePricePair:
n_from: uint256
n_to: 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)
Admin Ownership¶
admin
¶
PriceAggregator.admin() -> address: view
Getter for the admin of the contract, which is the Curve DAO OwnershipAgent.
Returns: admin (address
).
Source code
set_admin
¶
PriceAggregator.set_admin(_admin: address):
Guarded Method
This function is only callable by the admin
of the contract.
Function to set a new admin.
Emits: SetAdmin
Input | Type | Description |
---|---|---|
_admin | address | new admin address |
Source code
Contract Info Methods¶
SIGMA
¶
PriceAggregator.SIGMA() -> uint256: view
Getter for the sigma value.
Returns: sigma (uint256
).
STABLECOIN
¶
PriceAggregator.STABLECOIN() -> address: view
Getter for the stablecoin contract address.
Returns: crvUSD contract (address
).
Source code
last_timestamp
¶
PriceAggregator.last_timestamp() -> uint256:
Getter for the latest timestamp. Variable is updated when price_w
is called.
Returns: timestamp (uint256
).
last_tvl
¶
PriceAggregator.last_tvl(arg0: uint256) -> uint256:
Getter for the total value locked of price pair (pool).
Returns: total value locked (uint256
).
Input | Type | Description |
---|---|---|
arg0 | uint256 | Index of the price pair |
TVL_MA_TIME
¶
PriceAggregator.TVL_MA_TIME() -> uint256: view
Getter for the time period for the calculation of the EMA prices.
Returns: timestamp (uint256
).
last_price
¶
PriceAggregator.last_price() -> uint256: view
Getter for the last price. This variable was set to \(10^{18}\) (1.00) when initializing the contract and is now updated every time calling price_w
.
Returns: last price (uint256
).
Source code
last_price: public(uint256)
@external
def __init__(stablecoin: address, sigma: uint256, admin: address):
STABLECOIN = stablecoin
SIGMA = sigma # The change is so rare that we can change the whole thing altogether
self.admin = admin
self.last_price = 10**18
self.last_timestamp = block.timestamp
@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
price_w
¶
PriceAggregator.price_w() -> uint256:
Function to calculate and write the price. If called successfully, updates last_tvl
, last_price
and last_timestamp
.
Returns: price (uint256
).
Source code
@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 = 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