Token scaling
Working with fixed-point math in Solidity presents a unique set of challenges that developers must navigate to ensure accurate and secure smart contract functionality.
In an effort to abstract this complexity, the Vault manages decimal and rate scaling internally, scaling all token balances and input values prior to being sent to the Pool. By doing this, we ensure consistency of rounding direction across all Custom Pool implementations, removing a significant amount of complexity from the pool and allowing it to focus primarily on its invariant implementation.
Decimal scaling
All token balances and input values are scaled to 18 decimal places prior to being sent to the Pool. Once scaled, these numbers are referred to internally as scaled18
.
Pool registration
During pool registration, the vault stores the tokenDecimalDiffs
for each token in the pool in the PoolConfig
bits. Refer to the full implementation here.
tokenDecimalDiffs[i] = uint8(18) - IERC20Metadata(address(token)).decimals();
A token with 6 decimals (USDC) would have a tokenDecimalDiff = 18 - 6 = 12
, and a token with 18 decimals (WETH) would have a tokenDecimalDiff = 18 - 18 = 0
. Note that tokens with more than 18 decimals would revert here with an arithmetic error.
Scaling factors
The tokenDecimalDiffs
are then used to calculate the decimal scalingFactors
for each token. This implementation can be found in the PoolConfigLib.
function getDecimalScalingFactors(
PoolConfigBits memory config,
uint256 numTokens
) internal pure returns (uint256[] memory) {
uint256[] memory scalingFactors = new uint256[](numTokens);
bytes32 tokenDecimalDiffs = bytes32(uint256(config.getTokenDecimalDiffs()));
for (uint256 i = 0; i < numTokens; ++i) {
uint256 decimalDiff = tokenDecimalDiffs.decodeUint(
i * PoolConfigConst.DECIMAL_DIFF_BITLENGTH,
PoolConfigConst.DECIMAL_DIFF_BITLENGTH
);
// This is equivalent to `10**(18+decimalsDifference)` but this form optimizes for 18 decimal tokens.
scalingFactors[i] = FixedPoint.ONE * 10 ** decimalDiff;
}
return scalingFactors;
}
References
To review the scaling implementations, refer to ScalingHelpers.sol.
You can review the logic flow of swap, addLiquidity and removeLiquidity to better understand how the vault manages token scaling.
Rate scaling
With the successful rollout of The Merge and the adoption of ERC-4626, the ecosystem has seen a proliferation of yield bearing tokens. Recognizing the pivotal role that LSTs will play in the liquidity landscape moving forward, Balancer seeks to position itself as the definitive yield-bearing hub in DeFi.
To facilitate the adoption of yield bearing liquidity, Balancer abstracts the complexity of managing LSTs by centralizing all rate scaling in the vault, providing all pools with uniform rate scaled balances and input values by default, drastically reducing LVR and ensuring that yield is not captured by arbitrage traders.
What is a token rate
The classical example of a token with a rate is Lido's wstETH. As stETH accrues value from staking rewards, the exchange rate of wstETH -> stETH grows over time.
How does the Balancer Vault utilize token rates
Besides decimal scaling a token's rate is taken into account in Balancer in the following scenarios:
- Price computation as part of Stable and Boosted pools
- Yield fee computation on tokens with rates
A token's rate is defined as an 18-decimal fixed point number. It represents the ratio of the token's value relative to that of its underlying. For example, a rate of 1.1e18 of rETH means that 1 rETH has the value of 1.1 ETH.
Creating a pool with tokens that have rates
On pool register a TokenConfig is provided for each of the pool's tokens. To define a token with a rate, specify the token type as TokenType.WITH_RATE
. Additionally, you must provide a rateProvider
address that implements the IRateProvider
interface. Refer to Token types for a detailed explanation on each token type.
Rate scaling usage
Rate scaling is used on every swap
, addLiquidity
and removeLiquidity
operation. If the token was registered as TokenType.WITH_RATE
, an external call to the Rate Provider is made via getRate
. If the TokenType.STANDARD
was selected, the rate is always 1e18
. These rates are used to upscale the amountGiven
in the Vault primitives.
Info
- With a swap, the known token amount is given in native decimals as
amountGivenRaw
AmountGivenRaw
is upscaledAmountGivenScaled18
is forwarded to the pool.- Rates are undone before calculating and returning either
amountIn
oramountOut
.
You can read more on the Rate Providers page.
Live balances
The term liveBalances
is used internally to refer to balances that have been:
- Decimal scaled - Upscaled to 18 decimals
- Rate scaled - Adjusted for token rates
- Yield fee adjusted - Had yield fees deducted.
Any token balances sent to the pool will always be in live balance form. This ensures consistency across all tokens and removes the burden of token scaling from the pool logic.