DeFiHardhatFoundry
250,000 USDC
View results
Submission Details
Severity: high
Valid

`LibDeltaB` incorrectly assumes that all tokens in Well have 18 decimals

Summary

LibUsdOracle.getUsdPrice() is expected to return how much Token is worth 1 USD. It returns price with 1e18 precission, for example 0.1e18 means that 1 USD worth 0.1 * 10 ** tokenDecimals of that token. In some case that's correct because 1e18 precision is correctly used in LibWell.sol calculations.

However LibDeltaB.sol incorrectly handles precision in its inner calculations and therefore deltaB mechanism will be broken as soon as Well with non-18 decimals token will be whitelisted.

Want to note that currently only Bean/ETH is supported, but any Well can be added by governance; and 18 decimals isn't mentioned in the requirements:
https://docs.bean.money/almanac/farm/sun#minting-whitelist

Vulnerability

LibUsdOracle.getUsdPrice() is used in LibDeltaB.sol, let me explain using the example of USDC.

Firstly, let's have a look on LibUsdOracle.getUsdPrice(). It will return 1e18 price for USDC because internal Oracle functions return price with 1e6 precission (like getTokenPriceFromExternal() does):

function getUsdPrice(address token, uint256 lookback) internal view returns (uint256) {
...
// 1e18 * 1e6 = 1e24.
uint256 tokenPrice = getTokenPriceFromExternal(token, lookback);
if (tokenPrice == 0) return 0;
@> return uint256(1e24).div(tokenPrice);
}

Secondly, for Bean/USDC Well LibWell.getRatiosAndBeanIndex() will return ratio 1e18 for USDC and 1e6 for Bean:

function getRatiosAndBeanIndex(
IERC20[] memory tokens,
uint256 lookback
) internal view returns (uint[] memory ratios, uint beanIndex, bool success) {
success = true;
ratios = new uint[](tokens.length);
beanIndex = type(uint256).max;
for (uint i; i < tokens.length; ++i) {
if (C.BEAN == address(tokens[i])) {
beanIndex = i;
@> ratios[i] = 1e6;
} else {
@> ratios[i] = LibUsdOracle.getUsdPrice(address(tokens[i]), lookback);
if (ratios[i] == 0) {
success = false;
}
}
}
require(beanIndex != type(uint256).max, "Bean not in Well.");
}

Thirdly, Well reserves and ratios are used to calculate the ideal amount of Bean in reserves via WellFunction:

function calculateDeltaBFromReserves(
address well,
uint256[] memory reserves,
uint256 lookback
) internal view returns (int256) {
IERC20[] memory tokens = IWell(well).tokens();
Call memory wellFunction = IWell(well).wellFunction();
@> (uint256[] memory ratios, uint256 beanIndex, bool success) = LibWell.getRatiosAndBeanIndex(
tokens,
lookback
);
return
int256(
@> IBeanstalkWellFunction(wellFunction.target).calcReserveAtRatioSwap(
reserves,
beanIndex,
ratios,
wellFunction.data
)
).sub(int256(reserves[beanIndex]));
}

With USDC ratio = 1e18, Bean ratio = 1e6, Bean reserve = 500e6, USDC ratio = 500e6 it must calculate result 500e6.
https://github.com/BeanstalkFarms/Basin/blob/master/src/functions/ConstantProduct2.sol#L84-L93

reserve = (reserves[i] * reserves[j]).mulDiv(ratios[j], ratios[i]).sqrt();

However actual result is much lower:
reserve = sqrt(500e6 * 500e6 * 1e6 / 1e18) = 500

Impact

deltaB will be incorrectly priced which breaks core peg mechanism of Bean.

Tools Used

Manual Review

Recommendations

Refactor LibDeltaB.calculateDeltaBFromReserves() to correctly calculate price from 1e18 ratios.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Appeal created

T1MOH Submitter
about 1 year ago
T1MOH Submitter
about 1 year ago
inallhonesty Lead Judge
12 months ago
inallhonesty Lead Judge 12 months ago
Submission Judgement Published
Validated
Assigned finding tags:

`getUsdPrice` returns mixed decimals

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.