Summary
The Reseed Field is responsible for reinitializing the field on the L2 on which Beanstalk deploys. However, the init function uses the wrong index query resulting in a complete DOS for initializing on the L2.
Vulnerability Details
As you can see below, in the nested for loop, it loops through accountPlots
at index i
rather than index j
.
This causes a complete a failure of reinitialization due to the wrong index query.
https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/4e0ad0b964f74a1b4880114f4dd5b339bc69cd3e/protocol/contracts/beanstalk/init/reseed/L2/ReseedField.sol#L49
* @notice Re-initializes the field.
* @param accountPlots the plots for each account
* @param totalPods The total number of pods on L1.
* @param harvestable The number of harvestable pods on L1.
* @param harvested The number of harvested pods on L1.
* @param initialTemperature the initial Temperature of the field.
*/
function init(
MigratedPlotData[] calldata accountPlots,
uint256 totalPods,
uint256 harvestable,
uint256 harvested,
uint256 fieldId,
uint8 initialTemperature
) external {
uint256 calculatedTotalPods;
for (uint i; i < accountPlots.length; i++) {
for (uint j; j < accountPlots[i].plots.length; i++) {
uint256 podIndex = accountPlots[i].plots[j].podIndex;
uint256 podAmount = accountPlots[i].plots[j].podAmounts;
s.accts[accountPlots[i].account].fields[fieldId].plots[podIndex] = podAmount;
s.accts[accountPlots[i].account].fields[fieldId].plotIndexes.push(podIndex);
emit MigratedPlot(accountPlots[i].account, podIndex, podAmount);
calculatedTotalPods += podAmount;
}
}
require(calculatedTotalPods == totalPods, "ReseedField: totalPods mismatch");
require(totalPods >= harvestable, "ReseedField: harvestable mismatch");
require(harvestable >= harvested, "ReseedField: harvested mismatch");
s.sys.field.pods = totalPods;
s.sys.field.harvestable = harvestable;
s.sys.field.harvested = harvested;
s.sys.weather.thisSowTime = type(uint32).max;
s.sys.weather.lastSowTime = type(uint32).max;
s.sys.weather.temp = initialTemperature;
}
POC
When running these tests, ADD the below getter function into reseedField for testing purposes
function getPlot(address account, uint256 fieldId, uint256 podIndex) external view returns (uint256) {
return s.accts\[account].fields\[fieldId].plots\[podIndex];
}
pragma solidity >=0.6.0 <0.9.0;
pragma abicoder v2;
import {TestHelper} from "test/foundry/utils/TestHelper.sol";
import {ReseedField} from "../../../contracts/beanstalk/init/reseed/L2/ReseedField.sol";
import {AppStorage} from "contracts/beanstalk/storage/AppStorage.sol";
contract ReseedFieldTest is TestHelper {
ReseedField internal reseedField;
function setUp() public {
reseedField = new ReseedField();
}
function testPartialInitialization() public {
ReseedField.MigratedPlotData\[] memory accountPlots = new ReseedField.MigratedPlotData\[]\(2);
accountPlots\[0].account = address(0x1);
accountPlots\[0].plots = new ReseedField.Plot\[]\(2);
accountPlots\[0].plots\[0] = ReseedField.Plot({podIndex: 1, podAmounts: 100});
accountPlots\[0].plots\[1] = ReseedField.Plot({podIndex: 2, podAmounts: 200});
accountPlots\[1].account = address(0x2);
accountPlots\[1].plots = new ReseedField.Plot\[]\(2);
accountPlots\[1].plots\[0] = ReseedField.Plot({podIndex: 3, podAmounts: 300});
accountPlots\[1].plots\[1] = ReseedField.Plot({podIndex: 4, podAmounts: 400});
uint256 totalPods = 1000;
uint256 harvestable = 500;
uint256 harvested = 100;
uint256 fieldId = 0;
uint8 initialTemperature = 10;
reseedField.init(accountPlots, totalPods, harvestable, harvested, fieldId, initialTemperature);
assertEq(reseedField.getPlot(address(0x1), fieldId, 1), 100, "Plot 1 should be 100");
assertEq(reseedField.getPlot(address(0x1), fieldId, 2), 200, "Plot 2 should be 200");
assertEq(reseedField.getPlot(address(0x2), fieldId, 3), 300, "Plot 3 should be 300");
assertEq(reseedField.getPlot(address(0x2), fieldId, 4), 400, "Plot 4 should be 400");
}
}
OUTPUT
Ran 1 test for test/foundry/reseed/ReseedFieldTest.t.sol:ReseedFieldTest
 
\[FAIL. Reason: panic: array out-of-bounds access (0x32)] testPartialInitialization() (gas: 150947)
Traces:
\[383903] ReseedFieldTest::setUp()
├─ \[328966] → new ReseedField\@0xc83a02f4761098514aE50013Ba713a79E39699F3
│ └─ ← \[Return] 1643 bytes of code
└─ ← \[Stop]
\[150947] ReseedFieldTest::testPartialInitialization()
├─ \[142590] ReseedField::init(\[MigratedPlotData({ account: 0x0000000000000000000000000000000000000001, plots: \[Plot({ podIndex: 1, podAmounts: 100 }), Plot({ podIndex: 2, podAmounts: 200 })] }), MigratedPlotData({ account: 0x0000000000000000000000000000000000000002, plots: \[Plot({ podIndex: 3, podAmounts: 300 }), Plot({ podIndex: 4, podAmounts: 400 })] })], 1000, 500, 100, 0, 10)
│ ├─ emit MigratedPlot(account: 0x0000000000000000000000000000000000000001, plotIndex: 1, pods: 100)
│ ├─ emit MigratedPlot(account: 0x0000000000000000000000000000000000000002, plotIndex: 3, pods: 300)
│ └─ ← \[Revert] panic: array out-of-bounds access (0x32)
└─ ← \[Revert] panic: array out-of-bounds access (0x32)
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 2.11ms (71.91µs CPU time)
Ran 1 test suite in 686.00ms (2.11ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/foundry/reseed/ReseedFieldTest.t.sol:ReseedFieldTest
\[FAIL. Reason: panic: array out-of-bounds access (0x32)] testPartialInitialization() (gas: 150947)
Encountered a total of 1 failing tests, 0 tests succeeded
Impact
Initialization of field will DOS resulting in farmers not being issued their plots on L2.
Tools Used
Foundry/Forge
Recommendations
Change the index query fro i
to j
and re run the test.
Ran 1 test for test/foundry/reseed/ReseedFieldTest.t.sol:ReseedFieldTest
[PASS] testPartialInitialization() (gas: 348997)
Traces:
[373273] ReseedFieldTest::setUp()
├─ [318353] → new ReseedField@0xc83a02f4761098514aE50013Ba713a79E39699F3
│ └─ ← [Return] 1590 bytes of code
└─ ← [Stop]
[348997] ReseedFieldTest::testPartialInitialization()
├─ [329820] ReseedField::init([MigratedPlotData({ account: 0x0000000000000000000000000000000000000001, plots: [Plot({ podIndex: 1, podAmounts: 100 }), Plot({ podIndex: 2, podAmounts: 200 })] }), MigratedPlotData({ account: 0x0000000000000000000000000000000000000002, plots: [Plot({ podIndex: 3, podAmounts: 300 }), Plot({ podIndex: 4, podAmounts: 400 })] })], 1000, 500, 100, 0, 10)
│ ├─ emit MigratedPlot(account: 0x0000000000000000000000000000000000000001, plotIndex: 1, pods: 100)
│ ├─ emit MigratedPlot(account: 0x0000000000000000000000000000000000000001, plotIndex: 2, pods: 200)
│ ├─ emit MigratedPlot(account: 0x0000000000000000000000000000000000000002, plotIndex: 3, pods: 300)
│ ├─ emit MigratedPlot(account: 0x0000000000000000000000000000000000000002, plotIndex: 4, pods: 400)
│ └─ ← [Stop]
├─ [712] ReseedField::getPlot(0x0000000000000000000000000000000000000001, 0, 1) [staticcall]
│ └─ ← [Return] 100
├─ [0] VM::assertEq(100, 100, "Plot 1 should be 100") [staticcall]
│ └─ ← [Return]
├─ [712] ReseedField::getPlot(0x0000000000000000000000000000000000000001, 0, 2) [staticcall]
│ └─ ← [Return] 200
├─ [0] VM::assertEq(200, 200, "Plot 2 should be 200") [staticcall]
│ └─ ← [Return]
├─ [712] ReseedField::getPlot(0x0000000000000000000000000000000000000002, 0, 3) [staticcall]
│ └─ ← [Return] 300
├─ [0] VM::assertEq(300, 300, "Plot 3 should be 300") [staticcall]
│ └─ ← [Return]
├─ [712] ReseedField::getPlot(0x0000000000000000000000000000000000000002, 0, 4) [staticcall]
│ └─ ← [Return] 400
├─ [0] VM::assertEq(400, 400, "Plot 4 should be 400") [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.92ms (248.74µs CPU time)