CryptoFromPoolsRate
This oracle contract integrates multiple oracles, applying stored_rates
to tokens with an existing rate oracle. By chaining two oracles together, it facilitates the creation of lending oracle contracts without requiring the collateral asset to be paired directly against crvUSD.
GitHub
The source code of the CryptoFromPoolsRate.vy
and all other oracle contracts can be found on GitHub.
Oracle Immutability
The oracle contracts are fully immutable. Once deployed, they cannot change any parameters, stop the price updates, or alter the pools used to calculate the prices. All relevant data required for the oracle to function is passed into the __init__
function during the deployment of the contract.
__init__
@external
def __init__(
pools: DynArray[Pool, MAX_POOLS],
borrowed_ixs: DynArray[uint256, MAX_POOLS],
collateral_ixs: DynArray[uint256, MAX_POOLS]
):
POOLS = pools
pool_count: uint256 = 0
no_arguments: DynArray[bool, MAX_POOLS] = empty(DynArray[bool, MAX_POOLS])
use_rates: DynArray[bool, MAX_POOLS] = empty(DynArray[bool, MAX_POOLS])
for i in range(MAX_POOLS):
if i == len(pools):
assert i != 0, "Wrong pool counts"
pool_count = i
break
# Find N
N: uint256 = 0
for j in range(MAX_COINS + 1):
success: bool = False
res: Bytes[32] = empty(Bytes[32])
success, res = raw_call(
pools[i].address,
_abi_encode(j, method_id=method_id("coins(uint256)")),
max_outsize=32, is_static_call=True, revert_on_failure=False)
if not success:
assert j != 0, "No coins(0)"
N = j
break
assert borrowed_ixs[i] != collateral_ixs[i]
assert borrowed_ixs[i] < N
assert collateral_ixs[i] < N
# Init variables for raw call
success: bool = False
# Check and record if pool requires coin id in argument or no
if N == 2:
res: Bytes[32] = empty(Bytes[32])
success, res = raw_call(
pools[i].address,
_abi_encode(empty(uint256), method_id=method_id("price_oracle(uint256)")),
max_outsize=32, is_static_call=True, revert_on_failure=False)
if not success:
no_arguments.append(True)
else:
no_arguments.append(False)
else:
no_arguments.append(False)
res: Bytes[1024] = empty(Bytes[1024])
success, res = raw_call(pools[i].address, method_id("stored_rates()"), max_outsize=1024, is_static_call=True, revert_on_failure=False)
stored_rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
if success and len(res) > 0:
stored_rates = _abi_decode(res, DynArray[uint256, MAX_COINS])
u: bool = False
for r in stored_rates:
if r != 10**18:
u = True
use_rates.append(u)
Oracle Price¶
The price is determined by combining two different oracle prices. When necessary, stored_rates
are used to adjust the final computed price from these combined oracles.
Example
For example, suppose the oracle price of Token/ETH is 0.05, and the oracle price of ETH/crvUSD is 1000. The computed price of Token/crvUSD would then be calculated as follows:
\(\text{price} = 0.05 \times 1000 = 50\)
This calculation indicates that one Token is equivalent to 50 crvUSD.
price
¶
CryptoFromPoolsRate.price() -> uint256
Getter for the price of the collateral denominated against the borrowed token. E.g. a market with pufETH as collateral and crvUSD borrowable, the price will return the pufETH price with regard to crvUSD.
Returns: oracle price (uint256
).
Source code
@external
@view
def price() -> uint256:
return self._unscaled_price() * self._stored_rate()[0] / 10**18
@internal
@view
def _unscaled_price() -> uint256:
_price: uint256 = 10**18
for i in range(MAX_POOLS):
if i >= POOL_COUNT:
break
p_borrowed: uint256 = 10**18
p_collateral: uint256 = 10**18
if NO_ARGUMENT[i]:
p: uint256 = POOLS[i].price_oracle()
if COLLATERAL_IX[i] > 0:
p_collateral = p
else:
p_borrowed = p
else:
if BORROWED_IX[i] > 0:
p_borrowed = POOLS[i].price_oracle(unsafe_sub(BORROWED_IX[i], 1))
if COLLATERAL_IX[i] > 0:
p_collateral = POOLS[i].price_oracle(unsafe_sub(COLLATERAL_IX[i], 1))
_price = _price * p_collateral / p_borrowed
return _price
@internal
@view
def _stored_rate() -> (uint256, bool):
use_rates: bool = False
rate: uint256 = 0
rate, use_rates = self._raw_stored_rate()
if not use_rates:
return rate, use_rates
cached_rate: uint256 = self.cached_rate
if cached_rate == 0 or cached_rate == rate:
return rate, use_rates
if rate > cached_rate:
return min(rate, cached_rate * (10**18 + RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp)) / 10**18), use_rates
else:
return max(rate, cached_rate * (10**18 - min(RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp), 10**18)) / 10**18), use_rates
@internal
@view
def _raw_stored_rate() -> (uint256, bool):
rate: uint256 = 10**18
use_rates: bool = False
for i in range(MAX_POOLS):
if i == POOL_COUNT:
break
if USE_RATES[i]:
use_rates = True
rates: DynArray[uint256, MAX_COINS] = POOLS[i].stored_rates()
rate = rate * rates[COLLATERAL_IX[i]] / rates[BORROWED_IX[i]]
return rate, use_rates
price_w
¶
CryptoFromPoolsRate.price_w() -> uint256
This function calculates and writes the price while updating cached_rate
and cached_timestamp
. It is invoked whenever the _exchange
function is called within the AMM contract of the lending market.
Returns: oracle price (uint256
).
Source code
@external
def price_w() -> uint256:
return self._unscaled_price() * self._stored_rate_w() / 10**18
@internal
@view
def _unscaled_price() -> uint256:
_price: uint256 = 10**18
for i in range(MAX_POOLS):
if i >= POOL_COUNT:
break
p_borrowed: uint256 = 10**18
p_collateral: uint256 = 10**18
if NO_ARGUMENT[i]:
p: uint256 = POOLS[i].price_oracle()
if COLLATERAL_IX[i] > 0:
p_collateral = p
else:
p_borrowed = p
else:
if BORROWED_IX[i] > 0:
p_borrowed = POOLS[i].price_oracle(unsafe_sub(BORROWED_IX[i], 1))
if COLLATERAL_IX[i] > 0:
p_collateral = POOLS[i].price_oracle(unsafe_sub(COLLATERAL_IX[i], 1))
_price = _price * p_collateral / p_borrowed
return _price
@internal
def _stored_rate_w() -> uint256:
rate: uint256 = 0
use_rates: bool = False
rate, use_rates = self._stored_rate()
if use_rates:
self.cached_rate = rate
self.cached_timestamp = block.timestamp
return rate
@internal
@view
def _stored_rate() -> (uint256, bool):
use_rates: bool = False
rate: uint256 = 0
rate, use_rates = self._raw_stored_rate()
if not use_rates:
return rate, use_rates
cached_rate: uint256 = self.cached_rate
if cached_rate == 0 or cached_rate == rate:
return rate, use_rates
if rate > cached_rate:
return min(rate, cached_rate * (10**18 + RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp)) / 10**18), use_rates
else:
return max(rate, cached_rate * (10**18 - min(RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp), 10**18)) / 10**18), use_rates
Rates¶
The oracle contract utilizes the stored_rates
from a stableswap pool and considers these rates accordingly. The application of these rates is governed by the USE_RATES
variable. If set to true
, the rates are applied; if set to false
, they are not.
Source code
RATE_MAX_SPEED: constant(uint256) = 10**16 / 60 # Max speed of Rate change
@internal
@view
def _stored_rate() -> (uint256, bool):
use_rates: bool = False
rate: uint256 = 0
rate, use_rates = self._raw_stored_rate()
if not use_rates:
return rate, use_rates
cached_rate: uint256 = self.cached_rate
if cached_rate == 0 or cached_rate == rate:
return rate, use_rates
if rate > cached_rate:
return min(rate, cached_rate * (10**18 + RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp)) / 10**18), use_rates
else:
return max(rate, cached_rate * (10**18 - min(RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp), 10**18)) / 10**18), use_rates
Based on the values of rate
and cached_rate
, specific calculations are required to account for changes in rates:
-
If the
cached_rate
is 0 (indicating that no rates need to be applied) or equal to the actualrate
(meaning no rate changes have occurred since the last update), there is no need to do additional calculations to obtain therate
value. -
If
rate > cached_rate
, the rate is calculated as follows: \(\text{rate} = \min\left(\text{rate}, \frac{\text{cached_rate} \times \left(10^{18} + \text{RATE_MAX_SPEED} \times (\text{block.timestamp} - \text{cached_timestamp})\right)}{10^{18}}\right)\) This calculation aims to smoothly increase the rate, capping it at a calculated maximum based on theRATE_MAX_SPEED
and the time elapsed since the last cache update. -
In all other cases, where the rate has decreased, it is calculated as follows: \(\text{rate} = \max\left(\text{rate}, \frac{\text{cached_rate} \times \left(10^{18} - \min\left(\text{RATE_MAX_SPEED} \times (\text{block.timestamp} - \text{cached_timestamp}), 10^{18}\right)\right)}{10^{18}}\right)\) This formula ensures that the rate does not decrease too rapidly, with the reduction bounded by a minimum that considers the
RATE_MAX_SPEED
and the elapsed time.
stored_rate
¶
CryptoFromPoolsRate.stored_rate() -> uint256
Getter for the stored rates fetched from a stableswap pool.
The stored_rate
is calculated the following:
Returns: stored rate (uint256
).
Source code
@external
@view
def stored_rate() -> uint256:
return self._stored_rate()[0]
@internal
@view
def _stored_rate() -> (uint256, bool):
use_rates: bool = False
rate: uint256 = 0
rate, use_rates = self._raw_stored_rate()
if not use_rates:
return rate, use_rates
cached_rate: uint256 = self.cached_rate
if cached_rate == 0 or cached_rate == rate:
return rate, use_rates
if rate > cached_rate:
return min(rate, cached_rate * (10**18 + RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp)) / 10**18), use_rates
else:
return max(rate, cached_rate * (10**18 - min(RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp), 10**18)) / 10**18), use_rates
@internal
@view
def _raw_stored_rate() -> (uint256, bool):
rate: uint256 = 10**18
use_rates: bool = False
for i in range(MAX_POOLS):
if i == POOL_COUNT:
break
if USE_RATES[i]:
use_rates = True
rates: DynArray[uint256, MAX_COINS] = POOLS[i].stored_rates()
rate = rate * rates[COLLATERAL_IX[i]] / rates[BORROWED_IX[i]]
return rate, use_rates
cached_rate
¶
CryptoFromPoolsRate.cached_rate() -> uint256: view
Getter for the cached rate. This value is updated whenever the price_w
method is called.
Returns: cached rate (uint256
).
Source code
cached_rate: public(uint256)
@external
def price_w() -> uint256:
return self._unscaled_price() * self._stored_rate_w() / 10**18
@internal
def _stored_rate_w() -> uint256:
rate: uint256 = 0
use_rates: bool = False
rate, use_rates = self._stored_rate()
if use_rates:
self.cached_rate = rate
self.cached_timestamp = block.timestamp
return rate
cached_timestamp
¶
CryptoFromPoolsRate.cached_timestamp() -> uint256: view
Getter for the cached timestamp. This value is updated whenever the price_w
method is called.
Returns: cached timestamp (uint256
)
Source code
cached_timestamp: public(uint256)
@external
def price_w() -> uint256:
return self._unscaled_price() * self._stored_rate_w() / 10**18
@internal
def _stored_rate_w() -> uint256:
rate: uint256 = 0
use_rates: bool = False
rate, use_rates = self._stored_rate()
if use_rates:
self.cached_rate = rate
self.cached_timestamp = block.timestamp
return rate
USE_RATES
¶
CryptoFromPoolsRate.USE_RATES(arg0: uint256) -> bool: view
Getter method to check wether the pool at index arg0
uses rates or not. Pool indices are fetched via POOLS
.
Returns: whether to apply rates or not (bool
).
Input | Type | Description |
---|---|---|
arg0 | uint256 | Pool index. |
Contract Info Methods¶
BORROWED_IX
¶
CryptoFromPoolsRate.BORROWED_IX(arg0: uint256) -> uint256: view
Getter for the index of the borrowed token in the chain together pools. If the oracle contract is for an asset that has a rate, this method will return the coin indices of the "base asset". E.g., for pufETH, this method will return the index of wstETH in the pools and later, when calculating the price of pufETH, the rates are applied.
Returns: coin index (uint256
).
Input | Type | Description |
---|---|---|
arg0 | uint256 | Pool index to check BORROWED_IX for. |
COLLATERAL_IX
¶
CryptoFromPoolsRate.COLLATERAL_IX(arg0: uint256) -> uint256: view
Getter for the index of the collateral token within the pool.
Returns: coin index (uint256
).
Input | Type | Description |
---|---|---|
arg0 | uint256 | Pool index to check COLLATERAL_IX for. |
POOLS
¶
CryptoFromPoolsRate.POOLS(arg0: uint256) -> address: view
Getter for the liquidity pools used in the oracle contract.
Returns: pool contract (address
).
Input | Type | Description |
---|---|---|
arg0 | uint256 | Pool index. |
POOL_COUNT
¶
CryptoFromPoolsRate.POOL_COUNT() -> uint256: view
Getter for the total amount of pools used in the oracle contract.
Returns: amount of pools (uint256
).
NO_ARGUMENT
¶
CryptoFromPoolsRate.NO_ARGUMENT(arg0: uin256) -> bool: view
Getter for the NO_ARGUMENT
storage variable. This is an additional variable to ensure the correct price oracle is fetched from a pool.
Returns: true or false (bool)
Input | Type | Description |
---|---|---|
arg0 | uint256 | Pool index. |