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) {
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,
uint256 fertilizerAmount,
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;
uint256 remainingBpf = amountToFertilize / s.sys.fert.activeFertilizer;
uint256 oldBpf = s.sys.fert.bpf;
uint256 newBpf = oldBpf + remainingBpf;
uint256 firstBpf = s.sys.fert.fertFirst;
uint256 deltaFertilized;
while (newBpf >= firstBpf) {
deltaFertilized += (firstBpf - oldBpf) * s.sys.fert.activeFertilizer;
if (LibFertilizer.pop()) {
oldBpf = firstBpf;
firstBpf = s.sys.fert.fertFirst;
remainingBpf = (amountToFertilize - deltaFertilized) / s.sys.fert.activeFertilizer;
newBpf = oldBpf + remainingBpf;
}
else {
s.sys.fert.bpf = uint128(firstBpf);
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;
}
}
s.sys.fert.bpf = uint128(newBpf);
deltaFertilized += (remainingBpf * s.sys.fert.activeFertilizer);
s.sys.fert.fertilizedIndex += deltaFertilized;
s.sys.fert.leftoverBeans = amountToFertilize - deltaFertilized;
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
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(""));
}