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;
}