DeFiHardhat
21,000 USDC
View results
Submission Details
Severity: high
Invalid

Heavy precision loss when calculating the amount to be redeemed would cost users

Summary

Currently, the redemption process leads to substantial precision loss, causing users to receive significantly less than anticipated for the tokens they provide. This arises from the sequence of operations used to calculate the redemption amount, particularly in the LibUnripe library. The calculation involves dividing underlyingAmount.mul(s.recapitalized) by totalUsdNeeded before multiplying by amount/supply, a method chosen to prevent a potential overflow of the calculation. However, this approach inadvertently shifts the burden of precision loss onto users, leading to discrepancies in the actual versus expected redemption amounts.

Vulnerability Details

Whenever calculating the amount to be finally redeemed there is a heavy precision loss due to the order of operations which leads to users receiving way less than they are to get for the amount of tokens they provide.

Going through the code snippets this is finally done to get the amount to be redeemed https://github.com/Cyfrin/2024-05-Beanstalk-3/blob/662d26f12ee219ee92dc485c06e01a4cb5ee8dfb/protocol/contracts/libraries/LibUnripe.sol#L166-L167

function getPenalizedUnderlying(
address unripeToken,
uint256 amount,
uint256 supply
) internal view returns (uint256 redeem) {
require(isUnripe(unripeToken), "not vesting");
AppStorage storage s = LibAppStorage.diamondStorage();
//(snip)
uint256 underlyingAmount = s.u[unripeToken].balanceOfUnderlying;
redeem = underlyingAmount.mul(s.recapitalized).div(totalUsdNeeded).mul(amount).div(supply);
//(snip)
}

Now keep in mind that logically we can conclude the value of totalUsdNeeded is going to be very large considering it's decimals and all, showcasing how heavy the precision that's going to be lost would be.

Now going through the BIP-X: Misc Improvements whose changes this update contest was based on, we can see that this method of calculating the redeem (i.e dividing underlyingAmount.mul(s.recapitalized by totalUsdNeeded before multiplying it by amount/supply was hinted to be done so as to ensure that the attempt at calculating the redeem value does not overflow and leads to a revert, Which also confirms our previous statement that indeed the totalUsdNeeded is to be considered as somewhat large. However doing it this way showcases protocol is solving the potential overflow issue by unfairly delegating the cost to users, since their assets would be deducted where they can easily just break the calculation and have a numerator denominator pattern.

Keep in mind that this function is directly queried whenever there is a need to chop unripe tokens to ripe tokens in LibChop.chop()

function chop(
address unripeToken,
uint256 amount,
uint256 supply
) internal returns (address underlyingToken, uint256 underlyingAmount) {
AppStorage storage s = LibAppStorage.diamondStorage();
//@audit
underlyingAmount = LibUnripe.getPenalizedUnderlying(unripeToken, amount, supply);
// remove the underlying amount and decrease s.recapitalized if token is unripe LP
LibUnripe.removeUnderlying(unripeToken, underlyingAmount);
underlyingToken = s.u[unripeToken].underlyingToken;
}

Impact

Precision loss would constantly affect the amount of the tokens that gets redeemed for a user chopping their assets.

Tools Used

Manual review

Recommendations

Either bring all the multiplications ahead of the division, or break the calculation into two parts, implement it as something as:

// Calculate the numerator
uint256 numerator = s.u[unripeToken].balanceOfUnderlying.mul(s.recapitalized).mul(amount);
// Calculate the denominator
uint256 denominator = totalUsdNeeded.mul(supply);
// Perform the division to calculate redeem
redeem = numerator.div(denominator);
Updates

Lead Judging Commences

giovannidisiena Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Known issue

Support

FAQs

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