Rate Providers

Overview

Rate Providers are contracts that provide an exchange rate between two assets. These exchange rates can come from any on-chain source, whether that may be an oracle, a ratio of queryable balances, or another calculation.

Rate Providers implement a getRate() function that returns an exchange rate.

Use Cases

You can use rateProviders for all, some, or none of the assets in your pool. If you are not using a rateProvider for an asset, you must pass the zero address (0x0000000000000000000000000000000000000000), which will result in a rate of 1.

All Assets

You will want to use rateProviders for all assets in your pool when each asset has its own price that is independent of all the other assets' prices. If we have tokens A, B, and C and only have price feeds with respect to USD, then we would want all assets to have price feeds. When internally calculating relative prices, the USD would cancel out, giving us prices for A:B, A:C, B:C, and their inverses.

Some Assets

You will want to use rateProviders for some assets in your pool when you have rates that directly convert between the assets. If we have tokens A and B and a rate provider that gives the price of A with respect to B, then the rateProvider corresponding to token A would get the A:B price feed, and the rateProvider corresponding to token B would be the zero address.

None of the Assets

You will have no rateProviders in your pool when your tokens are price-pegged to each other. For example, a pool with USDC, USDT, and DAI would have all rateProviders set to the zero address since the exchange rate between those tokens is 1.

Examples

Direct Balance Query

Wrapping rebasing tokens, such as stETH, makes them compatible with Balancer, but knowing the exchange rate between the underlying rebasing token and the wrapped token is necessary to facilitate Stableswap swaps. As such, the wstETH rateProvider has a getRate() function that calls wstETH's own stEthPerToken() function. See the contract hereopen in new window.

Oracles

Using oracles for price feeds is a simple way to determine an exchange rate. There are two example contracts for how to use Chainlink as a price source: ChainlinkRegistryRateProvideropen in new window and ChainlinkRateProvideropen in new window.

ChainlinkRegistryRateProvideropen in new window

This contract makes use of Chainlink's registry contract so it can handle if Chainlink migrates to a new price feed for a given asset pair. Though there are increased gas costs for this, its a tradeoff for ensuring the pool doesn't get stuck on an abandoned price feed. While this is an unlikely scenario, it doesn't hurt to be careful.

ChainlinkRateProvideropen in new window

If you're running on a network for which Chainlink doesn't have a registry and you think the risk of a deprecated price feed is low enough, then you can use the rateProvider that directly queries a given Chainlink oracle.

Application of Rate Providers By Pool Type

Different types of pools utilize rate providers in different contexts.

PoolTypeYield FeePricing Equations
Composable Stable Pool
Meta Stable (EOL)
Weighted Pool
Managed Pool
Liquity Bootstrapping Pool

RateProvider Usage

While both Stable and Weighted Pools can have RateProviders, only Stable Pools use them to determine token prices.

  • Stable: raw token balances are scaled by the RateProvider values before being fed into StableMath
  • Weighted: token rates are used only to determine growth to calculate yield protocol fees

Composable Stable Pools and Meta Stable Pools have different implementations for the scaling operations but the outcome is the same.

Composable Stable Pool Implementation

Scaling Example:

function _scalingFactors() internal view virtual override returns (uint256[] memory) {
    // There is no need to check the arrays length since both are based on `_getTotalTokens`
    uint256 totalTokens = _getTotalTokens();
    uint256[] memory scalingFactors = new uint256[](totalTokens);

    for (uint256 i = 0; i < totalTokens; ++i) {
        scalingFactors[i] = _getScalingFactor(i).mulDown(_getTokenRate(i));
    }

    return scalingFactors;
}

Composable Stable Pool Swap Example

Looking at a swap of WETH to sfrxETHopen in new window the pool balances are upscaled.

Tokenbalancesratescaled balance
BPT - 0x5aee1e99fe86960377de9f88689616916d5dcabe25961484292658484319545823593205901000000000000000002596148429265848431954582359320590
wstETH - 0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0699833168467457018758011232653995744621757860983836140600107855
sfrxETH - 0xac3e018457b222d93114458476f3e3416abbe38f738895896174597740452610383719946558236417672488055538194248508
rETH - 0xae78736cd615f374d3085123a210448e74fc6393595050795188243854895010698819350879941996366340962316480428138

The upscaled token balances are are fed into _calcOutGivenIn.

ComposableStablePool._calcOutGivenIn
    (
        amplificationParameter = 200000, 
        balances = [7860983836140600107855,7672488055538194248508,6366340962316480428138], 
        tokenIndexIn = 1, 
        tokenIndexOut = 0, 
        tokenAmountIn = 163517835854389679,
        invariant = 21899336949210774987256
    )
=> (163536626614409153)

More details on this specific transaction can be found here.open in new window

Meta Stable Pool Implementation

Scaling Example:

function _scalingFactor(IERC20 token) internal view virtual override returns (uint256) {
    uint256 baseScalingFactor = super._scalingFactor(token);
    uint256 priceRate = _priceRate(token);
    // Given there is no generic direction for this rounding, it simply follows the same strategy as the BasePool.
    return baseScalingFactor.mulDown(priceRate);
}

Meta Stable Pool Swap Example

Looking at a swap of 50 ETH to 46.68 rETH the pool balances are upscaled.

Tokenbalancesratescaled balance
rEth 0xae78736cd615f374d3085123a210448e74fc639320040415915824227571764107012175115460930921445684973708525874136
WETH 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc221953505292747563228232100000000000000000021953505292747563228232

The upscaled token balances are fed into _calcOutGivenIn

MetaStablePool._calcOutGivenIn
    (
        amplificationParameter = 50000, 
        balances = [21445684973708525874136,21953505292747563228232], 
        tokenIndexIn = 1, 
        tokenIndexOut = 0, 
        tokenAmountIn = 49980000000000000000
    )
=> (49954807369169689518)

More details on this specific transaction can be found here.open in new window

Scaling

Bear in mind that the tokens used for demonstration in these examples all have 18 decimals and Balancer natively uses 18 decimals for internal accounting. If tokens have different decimals, the scaled balances scale with the tokens decimals as well.

Yield Fee

Rate providers play a crucial role in determining whether yield fees are charged during pool joins and exits. The pool's _athRateProduct and its dynamically calculated rateProduct are key factors that determine this.

rateProduct is calculated as the weighted product of all current rates:

Yield Fees for WeightedPools

TokenWeightrate
A (yield bearing)0.31.01
B (non yield bearing)0.51.00
C (yield bearing)0.21.05

rateProduct =

As part of the calculation of the rateProduct, the rateProvider of the pool tokens are queried for their rates.

/**
 * @notice Returns the contribution to the total rate product from a token with the given weight and rate Provider.
 */
function _getRateFactor(uint256 normalizedWeight, IRateProvider provider) internal view returns (uint256) {
    return provider == IRateProvider(0) ? FixedPoint.ONE : provider.getRate().powDown(normalizedWeight);
}

More details on the implementation can be found here.open in new window

There are several scenarios in which no yield fees are paid during a pools join or exit operation. Below are a couple of examples:

  • rateProduct remain unchanged: If multiple joins or exits occur without any factors contributing to a newATHRateProduct, no yield fees are minted.
  • Insignificant rateProduct fluctuations between tokens: In some cases, the rate of one token may increase while the rate of another token decreases. If, on a normalized basis, the rate increase of Token A is less than the rate decrease of Token B, the calculated rateProduct would not reach the ceiling of ATHRateProduct. As a result, no yield fees would be paid during the pools join or exit.

Yield fees being minted indicated by positive return values of

WeightedPool._getYieldProtocolFeesPoolPercentage
    (
        normalizedWeights = ["800000000000000000","200000000000000000"]
    )
=>
(1074881576209740, 978863663373687295)

More details on this specific transaction be found here.open in new window

No yield fees being minted indicated by zero return values of

WeightedPool.getYieldProtocolFeesPoolPercentage
    (
        normalizedWeights = ["800000000000000000","200000000000000000"]
    )
=>
(0, 0)

Whereas in this transactionopen in new window, the ATHRateProduct did not increase and (0,0) is returnedopen in new window since rateProduct <= athRateProduct. This indicates that no yield fees were paid in this transaction due to the insufficient increase in the rateProduct.