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

Lock of shipping beans in case of higher than total cap of routes

Summary

During shipping if the amount to be shipped is larger than sum of the cap of all the routes, then some beans will not be shipped and will be remained in the protocol unreachable.

Vulnerability Details

When the function ship is called, the amount of beansToShip would be distributed across all active shipping routes.
https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/df2dd129a878d16d4adc75049179ac0029d9a96b/protocol/contracts/libraries/LibShipping.sol#L28C14-L28C18

The issue is that if the amount to be shipped is larger than sum of the cap of all the routes, the remaining beans will not be shipped, and they will be locked in the protocol.

For simplicity assumes that we have only the following two shipment plans.

  • shipmentPlans[0] = ShipmentPlan{points: 10, cap: 500}

  • shipmentPlans[1] = ShipmentPlan{points: 10, cap: 400}

If beansToShip is equal to 1000, in the first iteration of the outer for-loop, we will have:

  • shipmentAmounts[1] = 400

  • remainingBeansToShip = 1000 - 400 = 600

  • totalPoints = 20 - 10 = 10

  • shipmentPlans[1].points = 0

In the second interaion of the outer for-loop, we will have:

  • shipmentAmounts[2] = 500

  • remainingBeansToShip = 600 - 500 = 100

  • totalPoints = 10 - 10 = 0

  • shipmentPlans[2].points = 0

Then the last for-loop will be executed where the shipmentAmounts will be shipped to the routes. It shows that 500 and 400 will be shipped to the first and the second shipment routes, respectively. While, the total beans to be shipped was 1000. So, 100 beans are not shipped and locked in the protocol.

PoC

In the following simplified version of the code, the emitted event LogPoc is 100 which shows the amount of beans that are remained and not shipped.

To run the PoC, first the function setShipmentRoutes, should be called, and then ship(1000).

// SPDX-License-Identifier: MIT
pragma solidity 0.8.22;
pragma solidity ^0.8.20;
contract LibShippingPoC {
struct ShipmentPlan {
uint256 points;
uint256 cap;
}
struct ShipmentRoute {
address planContract;
bytes4 planSelector;
ShipmentRecipient recipient;
bytes data;
}
enum ShipmentRecipient {
NULL,
SILO,
FIELD,
BARN
}
ShipmentRoute[] shipmentRoutes;
function setShipmentRoutes() public {
shipmentRoutes.push(
ShipmentRoute({
planContract: address(0),
planSelector: bytes4(0),
recipient: ShipmentRecipient.NULL,
data: ""
})
);
shipmentRoutes.push(
ShipmentRoute({
planContract: address(0),
planSelector: bytes4(0),
recipient: ShipmentRecipient.NULL,
data: ""
})
);
}
event LogPoc(uint256);
function ship(uint256 beansToShip) public {
uint256 remainingBeansToShip = beansToShip;
ShipmentRoute[] memory shipmentRoutes = shipmentRoutes;
ShipmentPlan[] memory shipmentPlans = new ShipmentPlan[](
shipmentRoutes.length
);
uint256[] memory shipmentAmounts = new uint256[](shipmentRoutes.length);
uint256 totalPoints;
(shipmentPlans, totalPoints) = getShipmentPlans(shipmentRoutes);
// May need to calculate individual stream rewards multiple times, since
// they are dependent on each others caps. Once a cap is reached, excess Beans are
// spread to other streams, proportional to their points.
for (uint256 i; i < shipmentRoutes.length; i++) {
bool capExceeded;
// Calculate the amount of rewards to each stream. Ignores cap and plans with 0 points.
getBeansFromPoints(
shipmentAmounts,
shipmentPlans,
totalPoints,
remainingBeansToShip
);
// Iterate though each stream, checking if cap is exceeded.
for (uint256 j; j < shipmentAmounts.length; j++) {
// If shipment amount exceeds plan cap, adjust plan and totals before recomputing.
if (shipmentAmounts[j] > shipmentPlans[j].cap) {
shipmentAmounts[j] = shipmentPlans[j].cap;
remainingBeansToShip -= shipmentPlans[j].cap;
totalPoints -= shipmentPlans[j].points;
shipmentPlans[j].points = 0;
capExceeded = true;
}
}
// If no cap exceeded, amounts are final.
if (!capExceeded) break;
}
// Ship it.
for (uint256 i; i < shipmentAmounts.length; i++) {
// if (shipmentAmounts[i] == 0) continue;
// LibReceiving.receiveShipment(
// shipmentRoutes[i].recipient,
// shipmentAmounts[i],
// shipmentRoutes[i].data
// );
}
emit LogPoc(remainingBeansToShip);
}
/**
* @notice Determines the amount of Beans to distribute to each shipping route based on points.
* @dev Does not factor in route cap.
* @dev If points are 0, does not alter the associated shippingAmount.
* @dev Assumes shipmentAmounts and shipmentRoutes have matching shape and ordering.
*/
function getBeansFromPoints(
uint256[] memory shipmentAmounts,
ShipmentPlan[] memory shipmentPlans,
uint256 totalPoints,
uint256 beansToShip
) public pure {
for (uint256 i; i < shipmentPlans.length; i++) {
// Do not modify amount for streams with 0 points. They either are zero or have already been set.
if (shipmentPlans[i].points == 0) continue;
shipmentAmounts[i] =
(beansToShip * shipmentPlans[i].points) /
totalPoints; // round down
}
}
/**
* @notice Gets the shipping plan for all shipping routes.
* @dev Determines which routes are active and how many Beans they will receive.
* @dev GetPlan functions should never fail/revert. Else they will have no Beans allocated.
*/
function getShipmentPlans(ShipmentRoute[] memory shipmentRoutes)
public
view
returns (ShipmentPlan[] memory shipmentPlans, uint256 totalPoints)
{
shipmentPlans = new ShipmentPlan[](shipmentRoutes.length);
shipmentPlans[0] = ShipmentPlan({points: 10, cap: 500});
shipmentPlans[1] = ShipmentPlan({points: 10, cap: 400});
totalPoints = 10 + 10;
}
}

Output is:

"topic": "0xbde9eb98b7c82180b784233bec929328e9ac2fc9927cea4e8145e7ff2db33d5a",
"event": "LogPoc",
"args": {
"0": "100"
}

Impact

Lock of to-be-shipped beans in case of being higher than the total cap of the routes.

Tools Used

Recommendations

It is recomended that when such case happens, the remained beans be transferred to an authorized contract to not be out of the circulation.

Updates

Lead Judging Commences

inallhonesty Lead Judge 12 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

`LibShipping` library fails to redistribute or report excess Beans when Beans > Cap

Support

FAQs

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