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

LibUnripe:getPenalizedUnderlying suffers from precision loss

Summary

LibUnripe:getPenalizedUnderlying, updated in the latest BIP, contains a case of division before multiplication which lowers the penalized amount of Ripe Tokens corresponding to the amount of Unripe Tokens that are Chopped according to the current Chop Rate.

Vulnerability Details

The getPenalizedUnderlying function computes the redeem variable on L167 by multiplying the underlyingAmount with s.recapitalized then divides the result by totalUsdNeeded the multiplies the result with amount and divides again by the supply.

(function comments removed to improve readability)

function getPenalizedUnderlying(
address unripeToken,
uint256 amount,
uint256 supply
) internal view returns (uint256 redeem) {
require(isUnripe(unripeToken), "not vesting");
AppStorage storage s = LibAppStorage.diamondStorage();
uint256 totalUsdNeeded = unripeToken == C.UNRIPE_LP ? LibFertilizer.getTotalRecapDollarsNeeded(supply)
: LibFertilizer.getTotalRecapDollarsNeeded();
uint256 underlyingAmount = s.u[unripeToken].balanceOfUnderlying;
@> redeem = underlyingAmount.mul(s.recapitalized).div(totalUsdNeeded).mul(amount).div(supply);
if(redeem > underlyingAmount) redeem = underlyingAmount;
}

This causes the redeem (which is the function return) to be lower than it should be, because the division by totalUsdNeeded truncates the result before the multiplication with amount. This returned value is used throughout the UnripeFacet.sol in the following functions:

and in LibChopConvert.sol:

All these affect the converting unripe -> ripe.

Impact

Impact - The loss of precision is small, thus the real impact, even if the return value is used everywhere when converting unripe -> ripe, is low.

Likelihood - This happens every time one of the affected functions is called.

The severity is Medium.

Tools Used

Manual review.

Recommendations

Implement the following modification:

function getPenalizedUnderlying(
address unripeToken,
uint256 amount,
uint256 supply
) internal view returns (uint256 redeem) {
require(isUnripe(unripeToken), "not vesting");
AppStorage storage s = LibAppStorage.diamondStorage();
// getTotalRecapDollarsNeeded() queries for the total urLP supply which is burned upon a chop
// If the token being chopped is unripeLP, getting the current supply here is inaccurate due to the burn
// Instead, we use the supply passed in as an argument to getTotalRecapDollarsNeeded since the supply variable
// here is the total urToken supply queried before burnning the unripe token
uint256 totalUsdNeeded = unripeToken == C.UNRIPE_LP ? LibFertilizer.getTotalRecapDollarsNeeded(supply)
: LibFertilizer.getTotalRecapDollarsNeeded();
// chop rate = total redeemable * (%DollarRecapitalized)^2 * share of unripe tokens
// redeem = totalRipeUnderlying * (usdValueRaised/totalUsdNeeded)^2 * UnripeAmountIn/UnripeSupply;
// But totalRipeUnderlying = CurrentUnderlying * totalUsdNeeded/usdValueRaised to get the total underlying
// redeem = currentRipeUnderlying * (usdValueRaised/totalUsdNeeded) * UnripeAmountIn/UnripeSupply
uint256 underlyingAmount = s.u[unripeToken].balanceOfUnderlying;
-- redeem = underlyingAmount.mul(s.recapitalized).div(totalUsdNeeded).mul(amount).div(supply);
++ redeem = underlyingAmount.mul(s.recapitalized).mul(amount).div(totalUsdNeeded).div(supply);
// cap `redeem to `balanceOfUnderlying in the case that `s.recapitalized` exceeds `totalUsdNeeded`.
// this can occur due to unripe LP chops.
if(redeem > underlyingAmount) redeem = underlyingAmount;
}
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.