Skip to content

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
>>> CryptoFromPoolsRate.price()
3110715001971074513929

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
>>> CryptoFromPoolsRate.price_w()
3110715001971074513929

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 actual rate (meaning no rate changes have occurred since the last update), there is no need to do additional calculations to obtain the rate 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 the RATE_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:

\[\text{rate} = 10^{18} * \frac{\text{stored_rates[collateral_ix]}}{\text{stored_rates[borrowed_ix]}}\]

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
>>> StableSwap.stored_rates()
1009207839112594800, 1166115485922357109

>>> CryptoFromPoolsRate.stored_rate()
865444161659808698              # calculated via: 1009207839112594800 / 1166115485922357109

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
>>> CryptoFromPoolsRate.cached_rate()
865388487987562874

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
>>> CryptoFromPoolsRate.cached_timestamp()
1714877987

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.
Source code
USE_RATES: public(immutable(DynArray[bool, MAX_POOLS]))
>>> CryptoFromPoolsRate.USE_RATES(0)    # checks for pufETH/wETH pool
'true'                                  # true, because the `stored_rates` of this pool are used

>>> CryptoFromPoolsRate.USE_RATES(0)    # checks for tryLSD pool
'false'                                 # false, because no rates are used

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.
Source code
BORROWED_IX: public(immutable(DynArray[uint256, MAX_POOLS]))
>>> CryptoFromPoolsRate.BORROWED_IX(0)
1                   # wstETH

>>> CryptoFromPoolsRate.BORROWED_IX(1)
0                   # wstETH

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.
Source code
COLLATERAL_IX: public(immutable(DynArray[uint256, MAX_POOLS]))
>>> CryptoFromPoolsRate.COLLATERAL_IX(0)
0                   # pufETH

>>> CryptoFromPoolsRate.COLLATERAL_IX(1)
2                   # wstETH

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.
Source code
POOLS: public(immutable(DynArray[Pool, MAX_POOLS]))
>>> CryptoFromPoolsRate.POOLS(0)
'0xEEda34A377dD0ca676b9511EE1324974fA8d980D'

>>> CryptoFromPoolsRate.POOLS(1)
'0x2889302a794dA87fBF1D6Db415C1492194663D13'

POOL_COUNT

CryptoFromPoolsRate.POOL_COUNT() -> uint256: view

Getter for the total amount of pools used in the oracle contract.

Returns: amount of pools (uint256).

Source code
POOL_COUNT: public(immutable(uint256))
>>> CryptoFromPoolsRate.POOL_COUNT()
2

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.
Source code
NO_ARGUMENT: public(immutable(DynArray[bool, MAX_POOLS]))
>>> CryptoFromPoolsRate.NO_ARGUMENT(0)
'false'

>>> CryptoFromPoolsRate.NO_ARGUMENT(1)
'false'