Vulnerability Details
This function loops through stems until it withdraws amount maxTokens. As you can see it skips germinating stems. Problem is that it doesn't reset amount of germination stem.
https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/df2dd129a878d16d4adc75049179ac0029d9a96b/protocol/contracts/libraries/Convert/LibConvert.sol#L466-L469
function _withdrawTokens(
address token,
int96[] memory stems,
uint256[] memory amounts,
uint256 maxTokens
) internal returns (uint256, uint256) {
...
{
a.bdvsRemoved = new uint256[](stems.length);
a.stalksRemoved = new uint256[](stems.length);
a.depositIds = new uint256[](stems.length);
LibGerminate.GermStem memory germStem = LibGerminate.getGerminatingStem(token);
while ((i < stems.length) && (a.active.tokens < maxTokens)) {
...
@> if (germStem.germinatingStem <= stems[i]) {
i++;
continue;
}
...
}
@> for (i; i < stems.length; ++i) amounts[i] = 0;
@> emit RemoveDeposits(user, token, stems, amounts, a.active.tokens, a.bdvsRemoved);
@> emit LibSilo.TransferBatch(user, user, address(0), a.depositIds, amounts);
}
...
}
Suppose a scenario:
User submits stems = [germinatingStem, germinatingStem, oldStem].
First 2 values will be skipped in cycle, and last works correctly.
for cycle won't clear amounts of those germinating stems because they were looped.
As a result LibSilo.TransferBatch will be emitted with arbitrary amount and 0 tokenId.
Impact
Events RemoveDeposits and TransferBatch will be emitted with incorrect values.
Tools Used
Manual Review
Recommendations
Reset amount before skipping germination stem, and set depositId in advance:
function _withdrawTokens(
address token,
int96[] memory stems,
uint256[] memory amounts,
uint256 maxTokens
) internal returns (uint256, uint256) {
...
// a bracket is included here to avoid the "stack too deep" error.
{
a.bdvsRemoved = new uint256[](stems.length);
a.stalksRemoved = new uint256[](stems.length);
a.depositIds = new uint256[](stems.length);
// get germinating stem and stemTip for the token
LibGerminate.GermStem memory germStem = LibGerminate.getGerminatingStem(token);
while ((i < stems.length) && (a.active.tokens < maxTokens)) {
+ a.depositIds[i] = uint256(LibBytes.packAddressAndStem(token, stems[i]));
...
if (germStem.germinatingStem <= stems[i]) {
i++;
+ amounts[i] = 0;
continue;
}
...
- a.depositIds[i] = uint256(LibBytes.packAddressAndStem(token, stems[i]));
i++;
}
for (i; i < stems.length; ++i) amounts[i] = 0;
emit RemoveDeposits(user, token, stems, amounts, a.active.tokens, a.bdvsRemoved);
emit LibSilo.TransferBatch(user, user, address(0), a.depositIds, amounts);
}
...
}