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

Beans will not be allocated to fertilizer buyers due to a difference of decimals

Summary

Beans will not be allocated to fertilizer buyers due to a difference of decimals

Relevant GitHub Links:

https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/main/protocol/contracts/libraries/LibReceiving.sol#L110

Vulnerability Details

When a user buys fertilizer, he has to give the amount of the barn token to provide in 18 decimal precision:

/**
* @notice Purchase Fertilizer from the Barn Raise with the Barn Raise token.
* @param tokenAmountIn Amount of tokens to buy Fertilizer with 18 decimal precision.
* @param minFertilizerOut The minimum amount of Fertilizer to purchase. Protects against a significant Barn Raise Token/USD price decrease.
* @param minLPTokensOut The minimum amount of LP tokens to receive after adding liquidity with Barn Raise tokens.
* @dev The # of Fertilizer minted is equal to the value of the Ether paid in USD.
*/
function mintFertilizer(
uint256 tokenAmountIn,
uint256 minFertilizerOut,
uint256 minLPTokensOut
) external payable fundsSafu noOutFlow returns (uint256 fertilizerAmountOut) {
...
}

With this tokenAmountIn, the amount of fertilizer to mint is computed with the following formula:

function _getMintFertilizerOut(
uint256 tokenAmountIn,
address barnRaiseToken
) public view returns (uint256 fertilizerAmountOut) {
// tokenAmountIn (18 decimals) / price (6 decimals) = 12 decimals
fertilizerAmountOut = tokenAmountIn.div(LibUsdOracle.getUsdPrice(barnRaiseToken));
}

It uses the computed price from the oracle with 6 decimals to determine the value of the deposited amount. The result of it is 12 decimals (fertilizer). With this same fertilizer amount it is added to the s.sys.fert.activeFertilizer:

function addFertilizer(
uint128 season,
uint256 tokenAmountIn, // 18 decimals
uint256 fertilizerAmount, // 12 decimals
uint256 minLP
) internal returns (uint128 id) {
...
s.sys.fert.activeFertilizer = s.sys.fert.activeFertilizer.add(fertilizerAmount);
...
}

This variable basically stores how much usd in 12 decimals have been provided to the barn and have not been rewarded yet.
Let's now see when a sunrise function is called and beans are shipped to the barn.

function barnReceive(uint256 shipmentAmount, bytes memory) private {
AppStorage storage s = LibAppStorage.diamondStorage();
uint256 amountToFertilize = shipmentAmount + s.sys.fert.leftoverBeans;
// Get the new Beans per Fertilizer and the total new Beans per Fertilizer
// Zeroness of activeFertilizer handled in Planner.
uint256 remainingBpf = amountToFertilize / s.sys.fert.activeFertilizer;
uint256 oldBpf = s.sys.fert.bpf;
uint256 newBpf = oldBpf + remainingBpf;
// Get the end BPF of the first Fertilizer to run out.
uint256 firstBpf = s.sys.fert.fertFirst;
uint256 deltaFertilized;
// If the next fertilizer is going to run out, then step BPF according
while (newBpf >= firstBpf) {
// Increment the cumulative change in Fertilized.
deltaFertilized += (firstBpf - oldBpf) * s.sys.fert.activeFertilizer; // fertilizer between init and next cliff
if (LibFertilizer.pop()) {
oldBpf = firstBpf;
firstBpf = s.sys.fert.fertFirst;
// Calculate BPF beyond the first Fertilizer edge.
remainingBpf = (amountToFertilize - deltaFertilized) / s.sys.fert.activeFertilizer;
newBpf = oldBpf + remainingBpf;
}
// Else, if there is no more fertilizer. Matches plan cap.
else {
s.sys.fert.bpf = uint128(firstBpf); // SafeCast unnecessary here.
s.sys.fert.fertilizedIndex += deltaFertilized;
require(amountToFertilize == deltaFertilized, "Inexact amount of Beans at Barn");
require(s.sys.fert.fertilizedIndex == s.sys.fert.unfertilizedIndex, "Paid != owed");
return;
}
}
// Distribute the rest of the Fertilized Beans
s.sys.fert.bpf = uint128(newBpf); // SafeCast unnecessary here.
deltaFertilized += (remainingBpf * s.sys.fert.activeFertilizer);
s.sys.fert.fertilizedIndex += deltaFertilized;
// There will be up to activeFertilizer Beans leftover Beans that are not fertilized.
// These leftovers will be applied on future Fertilizer receipts.
s.sys.fert.leftoverBeans = amountToFertilize - deltaFertilized;
// Confirm successful receipt.
emit Receipt(ShipmentRecipient.BARN, shipmentAmount, abi.encode(""));
}

The shipmentAmount and s.sys.fert.leftoverBeans will be in 6 decimal precision because of the ERC20 feature.
Looking at the remainingBpf we can see that amountToFertilizer which represents the total beans that can be distributed in the barn in 6 decimals is divided by the s.sys.fert.activeFertilizer which is the amount of usd to be fertilized in 12 decimal precision. Clearly this division will be almost everytime 0 due to the decimal difference.
As a result of this, the newBpf will remain the same and it will not exceed the firstBpf and no fertilizer will get any beans.

Imagine the following situation:
1- Someone provides 1 USD worth of the barn token with a humidity of 5 (for every fertilizer unit will get 5 beans). The id for his position will be s.sys.fert.bpf + 5000000
2- s.sys.fert.activeFertilizer will be 1e12
3- The barn receive 5 beans (5e6) which should fullfill user's position
4- RemainingBpf is computed as 5e6 / 1e12 = 0
5- s.sys.fert.bpf remains the same and the beans shipped to the barn goes to the s.sys.fert.leftoverBeans variable.

The amount of beans that the barn should receive in order to enter the while loop and allocate beans to users is:

newBpf >= firstBpf
firstBpf = s.sys.fert.bpf + 5 0000 000 // user's id
newBpf = s.sys.fert.bpf + (amountToFertilize / s.sys.fert.activeFertilizer)
amountToFertilize = 5 000 000 * s.sys.fert.activeFerilizer = 5 000 000 * 1 000 000 000 000 = 5 000 000 000 000 000 000 (5e18)

Since amountToFertilize is in beans, it has 6 decimals. Hence this amount will never be possible to reach 5000000 million beans.

Impact

High

Tools Used

Manual review

Recommendations

Add this decimal difference of the beans to the 18 decimals needed to reach the bpf.

function barnReceive(uint256 shipmentAmount, bytes memory) private {
AppStorage storage s = LibAppStorage.diamondStorage();
uint256 amountToFertilize = shipmentAmount + s.sys.fert.leftoverBeans;
// Get the new Beans per Fertilizer and the total new Beans per Fertilizer
// Zeroness of activeFertilizer handled in Planner.
- uint256 remainingBpf = amountToFertilize / s.sys.fert.activeFertilizer;
+ uint256 remainingBpf = amountToFertilize * (10 ** 12) / s.sys.fert.activeFertilizer;
uint256 oldBpf = s.sys.fert.bpf;
uint256 newBpf = oldBpf + remainingBpf;
// Get the end BPF of the first Fertilizer to run out.
uint256 firstBpf = s.sys.fert.fertFirst;
uint256 deltaFertilized;
// If the next fertilizer is going to run out, then step BPF according
while (newBpf >= firstBpf) {
// Increment the cumulative change in Fertilized.
deltaFertilized += (firstBpf - oldBpf) * s.sys.fert.activeFertilizer; // fertilizer between init and next cliff
if (LibFertilizer.pop()) {
oldBpf = firstBpf;
firstBpf = s.sys.fert.fertFirst;
// Calculate BPF beyond the first Fertilizer edge.
remainingBpf = (amountToFertilize - deltaFertilized) / s.sys.fert.activeFertilizer;
newBpf = oldBpf + remainingBpf;
}
// Else, if there is no more fertilizer. Matches plan cap.
else {
s.sys.fert.bpf = uint128(firstBpf); // SafeCast unnecessary here.
s.sys.fert.fertilizedIndex += deltaFertilized;
require(amountToFertilize == deltaFertilized, "Inexact amount of Beans at Barn");
require(s.sys.fert.fertilizedIndex == s.sys.fert.unfertilizedIndex, "Paid != owed");
return;
}
}
// Distribute the rest of the Fertilized Beans
s.sys.fert.bpf = uint128(newBpf); // SafeCast unnecessary here.
deltaFertilized += (remainingBpf * s.sys.fert.activeFertilizer);
s.sys.fert.fertilizedIndex += deltaFertilized;
// There will be up to activeFertilizer Beans leftover Beans that are not fertilized.
// These leftovers will be applied on future Fertilizer receipts.
s.sys.fert.leftoverBeans = amountToFertilize - deltaFertilized;
// Confirm successful receipt.
emit Receipt(ShipmentRecipient.BARN, shipmentAmount, abi.encode(""));
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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