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

Discrepancy in Token Burning and Calculation Order

Summary

There is a discrepancy in the order of burning unripe tokens and calculating their underlying value between the UnripeFacet::chop and ConvertFacet::convert functions.This inconsistency can lead to different redemption values for the same transaction parameters.

  • UnripeFacet::chop Function: This function burns the unripe tokens first and then calculates the underlying amount to redeem. The total supply of unripe tokens is reduced before calculating the underlying amount, which affects the calculation.

  • ConvertFacet::convert Function: This function calls LibChopConvert.convertUnripeToRipe, which first calculates the underlying amount and then burns the unripe tokens. The total supply remains unchanged during the calculation, resulting in a different underlying amount being determined.

Vulnerability Details

  1. In the chop Function: The unripe tokens are burned first, and then the underlying amount is calculated. The total supply of unripe tokens is reduced before calculating the underlying amount, which affects the calculation.

function chop(
address unripeToken,
uint256 amount,
LibTransfer.From fromMode,
LibTransfer.To toMode
) external payable nonReentrant returns (uint256) {
// burn the token from the msg.sender address
uint256 supply = IBean(unripeToken).totalSupply();
@>> amount = LibTransfer.burnToken(IBean(unripeToken), amount, msg.sender, fromMode);
// get ripe address and ripe amount
@>> (address underlyingToken, uint256 underlyingAmount) = LibChop.chop(
unripeToken,
amount,
supply
);
// send the corresponding amount of ripe token to the user address
require(underlyingAmount > 0, "Chop: no underlying");
IERC20(underlyingToken).sendToken(underlyingAmount, msg.sender, toMode);
// emit the event
emit Chop(msg.sender, unripeToken, amount, underlyingAmount);
return underlyingAmount;
}
  1. In the ConvertFacet::convert Function: In this function, LibChopConvert.convertUnripeToRipe is called via convertData param, which first calculates the underlying amount and then burns the unripe tokens. The total supply remains unchanged during the calculation, resulting in a different underlying amount being determined.

function convert(
bytes calldata convertData,
int96[] memory stems,
uint256[] memory amounts
) external payable nonReentrant returns (int96 toStem, uint256 fromAmount, uint256 toAmount, uint256 fromBdv, uint256 toBdv) {
uint256 grownStalk;
LibConvert.convertParams memory cp = LibConvert.convert(convertData);
///... internal state changes
@>> toAmount = cp.toAmount;
@>> fromAmount = cp.fromAmount;
//@>> convert from unripe token to ripe token
emit Convert(cp.account, cp.fromToken, cp.toToken, cp.fromAmount, cp.toAmount);
}

So, the kind is equal to LibConvertData.ConvertKind.UNRIPE_TO_RIPE.

else if (kind == LibConvertData.ConvertKind.UNRIPE_TO_RIPE) {
(cp.toToken, cp.fromToken, cp.toAmount, cp.fromAmount) = LibChopConvert.convertUnripeToRipe(convertData);
}

Here, the actual conversion from unripe to ripe occurs, but it calculates the underlying amount before burning the unripe tokens

function convertUnripeToRipe(bytes memory convertData)
internal
returns (
address tokenOut,
address tokenIn,
uint256 amountOut,
uint256 amountIn
)
{
// Decode convertdata
(amountIn, tokenIn) = convertData.lambdaConvert();
@>> (tokenOut, amountOut) = LibChop.chop(
tokenIn,
amountIn,
IBean(tokenIn).totalSupply()
);
@>> IBean(tokenIn).burn(amountIn);
}

This is a critical inconsistency in the chop and convert functions related to the order of burning unripe tokens and calculating the underlying amount. This can lead to different redemption values.

The calulation of the redeem value if the supply is change LibChop.chop() -->> LibUnripe::getPenalizedUnderlying()

The calulation of the redeem value.

// formula: redeem = currentRipeUnderlying * (usdValueRaised/totalUsdNeeded) * UnripeAmountIn/UnripeSupply;
redeem = underlyingAmount.mul(s.recapitalized).div(totalUsdNeeded).mul(amount).div(supply);

Impact on the value of redeem:

  • UnripeToken is not burned: redeem = 100 * 50 / 100 * 1 / 100 = 0.5

  • UnripeToken is burned: redeem = 100 * 50 / 100 * 1 / 99 = 0.505

Impact

The different order of operations can lead to inconsistent and miscalculations of the underlying amount to be redeemed, resulting in underlying amount discrepancies. An attacker could exploit this inconsistency to manipulate the amount of ripe tokens they receive.

chop Function: Results in a lower underlying amount due to reduced total supply after burning.

convertUnripeToRipe Function: Results in a higher underlying amount as the total supply used in calculations does not account for the burned tokens.

if redeem value is > then underlyingAmount this will cause a significant difference

// this can occur due to unripe LP chops.
if(redeem > underlyingAmount) redeem = underlyingAmount;

Tools Used

Manual Review

Recommendations

Calculate the underlying amount first, then burn the tokens, and use the initial total supply for calculation in both cases.

Updates

Lead Judging Commences

giovannidisiena Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
hunter_w3b Submitter
about 1 year ago
hunter_w3b Submitter
about 1 year ago
giovannidisiena Lead Judge
about 1 year ago
giovannidisiena Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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