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

`LibPipelineConvert.executePipelineConvert()` doesn't decrease Grown Stalk when BDV decreases

Summary

Pipeline Convert is a way to convert Silo deposits from one token to another while not losing Grown Stalk and potentially increasing final BDV.

Problem is that it doesn't handle case where final BDV is less than before Pipeline Convert. Because of issue it doesn't decrease Grown Stalk.
It allows users to just withdraw part of their deposit without Grown Stalk penalty.

Vulnerability Details

Let's take a look on pipelineConvert(). It:

  1. Removes deposits from account

  2. Execute Pipeline Convert logic

  3. Creates deposits to account with certain BDV and Grown Stalk which are calculated inside step 2
    https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/df2dd129a878d16d4adc75049179ac0029d9a96b/protocol/contracts/beanstalk/silo/PipelineConvertFacet.sol#L62-L110

function pipelineConvert(
address inputToken,
int96[] calldata stems,
uint256[] calldata amounts,
address outputToken,
AdvancedFarmCall[] calldata advancedFarmCalls
)
external
payable
fundsSafu
nonReentrant
returns (int96 toStem, uint256 fromAmount, uint256 toAmount, uint256 fromBdv, uint256 toBdv)
{
...
// withdraw tokens from deposits and calculate the total grown stalk and bdv.
uint256 grownStalk;
@> (grownStalk, fromBdv) = LibConvert._withdrawTokens(inputToken, stems, amounts, fromAmount);
@> (toAmount, grownStalk, toBdv) = LibPipelineConvert.executePipelineConvert(
inputToken,
outputToken,
fromAmount,
fromBdv,
grownStalk,
advancedFarmCalls
);
@> toStem = LibConvert._depositTokensForConvert(outputToken, toAmount, toBdv, grownStalk);
emit Convert(LibTractor._user(), inputToken, outputToken, fromAmount, toAmount);
}

Let's dive into step 2. It performs AdvancedFarmCalls and applies penalty to newGrownStalk based on caps and deltaB disbalance.

function executePipelineConvert(
address inputToken,
address outputToken,
uint256 fromAmount,
uint256 fromBdv,
uint256 initialGrownStalk,
AdvancedFarmCall[] calldata advancedFarmCalls
) external returns (uint256 toAmount, uint256 newGrownStalk, uint256 newBdv) {
...
IERC20(inputToken).transfer(C.PIPELINE, fromAmount);
executeAdvancedFarmCalls(advancedFarmCalls);
// user MUST leave final assets in pipeline, allowing us to verify that the farm has been called successfully.
// this also let's us know how many assets to attempt to pull out of the final type
toAmount = transferTokensFromPipeline(outputToken);
// Calculate stalk penalty using start/finish deltaB of pools, and the capped deltaB is
// passed in to setup max convert power.
pipeData.stalkPenaltyBdv = prepareStalkPenaltyCalculation(...);
// Update grownStalk amount with penalty applied
newGrownStalk = (initialGrownStalk * (fromBdv - pipeData.stalkPenaltyBdv)) / fromBdv;
newBdv = LibTokenSilo.beanDenominatedValue(outputToken, toAmount);
}

And in step 3 it calculates new Stem based on Grown Stalk it should produce:
https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/df2dd129a878d16d4adc75049179ac0029d9a96b/protocol/contracts/libraries/Silo/LibTokenSilo.sol#L545-L555

function calculateStemForTokenFromGrownStalk(
address token,
uint256 grownStalk,
uint256 bdv
) internal view returns (int96 stem, GerminationSide side) {
LibGerminate.GermStem memory germStem = LibGerminate.getGerminatingStem(token);
stem = germStem.stemTip.sub(
SafeCast.toInt96(SafeCast.toInt256(grownStalk.mul(PRECISION).div(bdv)))
);
side = LibGerminate._getGerminationState(stem, germStem);
}

Finally here is attack vector:

  1. Assume Attacker has Silo deposit of 100e6 Bean with Stem 4990e6. Current stemTip = 5000e6, it means his Grown Stalk is (5000e6 - 4990e6) * 100e6 / 1e6 = 1000e6

  2. In AdvancedFarmCalls during PipelineConvert Attacker just withdraws 99e6 Beans (converts Bean to Bean). It can only slightly be penalised based on cap exceed, penalization doesn't matter so assume penalization doesn't happen

  3. It calculates stem for deposit of 1e6 Bean so that it produced 1000e6 Stalk. As a result user has deposit of 1e6 Bean with stem = 5000e6 - 1000e6 * 1e6 / 1e6 = 4000e6.

As you can see his Grown Stalk was not decreased while user just performed withdrawal via PipelineConvert.

Impact

Users can withdraw significant part of their deposit without losing Grown Stalk. And repeat it again and again. This allows to have big amount of Stalk without depositing much to Silo.

Such attacker will steal Roots from fair users without risk. That's because he can make huge deposit for little time to gain huge amount of Stalk and Roots, and then withdraw that huge deposit without Grown Stalk penalization (as far as convert cap allows it).

Tools Used

Manual Review

Recommendations

Decrease Grown Stalk proportionally to decrease of BDV:

function executePipelineConvert(
address inputToken,
address outputToken,
uint256 fromAmount,
uint256 fromBdv,
uint256 initialGrownStalk,
AdvancedFarmCall[] calldata advancedFarmCalls
) external returns (uint256 toAmount, uint256 newGrownStalk, uint256 newBdv) {
...
- // Update grownStalk amount with penalty applied
- newGrownStalk = (initialGrownStalk * (fromBdv - pipeData.stalkPenaltyBdv)) / fromBdv;
-
- newBdv = LibTokenSilo.beanDenominatedValue(outputToken, toAmount);
+ newBdv = LibTokenSilo.beanDenominatedValue(outputToken, toAmount);
+ // Update grownStalk amount with penalty applied
+ newGrownStalk = (initialGrownStalk * (fromBdv - pipeData.stalkPenaltyBdv)) / fromBdv;
+ if (newBdv < fromBdv) newGrownStalk = newGrownStalk * newBdv / fromBdv;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Bypassing the penalty during converting in direction of unpegging

Appeal created

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

Bypassing the penalty during converting in direction of unpegging

Support

FAQs

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