Skip to content

Fix repayment field gas limit issue by splitting large plot accounts#177

Open
pocikerim wants to merge 4 commits intofrijo/release/PI-15from
fix/PI-15-gas-limit
Open

Fix repayment field gas limit issue by splitting large plot accounts#177
pocikerim wants to merge 4 commits intofrijo/release/PI-15from
fix/PI-15-gas-limit

Conversation

@pocikerim
Copy link
Contributor

Summary

Fix gas limit issue in repayment field initialization by splitting accounts with large plot counts across multiple transactions.

Problem

Accounts with ~240+ plots exceed the EIP-7987 transaction gas limit (16,777,216 gas). Each plot costs ~70,600 gas, so accounts exceeding this threshold cause transaction failures.

Affected accounts:

  • 4 whale accounts with 438-581 plots each
  • All would fail during populateBeanstalkField execution

Changes

1. contracts/beanstalk/tempFacets/TempRepaymentFieldFacet.sol

Fixed piIndex mapping to correctly track plot positions when an account receives plots across multiple transactions.

- s.accts[account].fields[REPAYMENT_FIELD_ID].piIndex[podIndex] = j;                                                                                                                                                             
+ s.accts[account].fields[REPAYMENT_FIELD_ID].piIndex[podIndex] =                                                                                                                                                                
+     s.accts[account].fields[REPAYMENT_FIELD_ID].plotIndexes.length - 1;

Why: The loop index j resets to 0 for each transaction. Using plotIndexes.length - 1 gives the actual array position.

2. utils/read.js

Added splitWhaleAccounts() function to split accounts exceeding 200 plots into multiple entries.

splitWhaleAccounts(plotData, maxPlotsPerEntry = 200) 

Why: 200 plots ≈ 14M gas, safely under the 16.7M limit with margin.

3. scripts/beanstalkShipments/populateBeanstalkField.js

Integrated whale splitting before chunking.

  const splitData = splitWhaleAccounts(rawPlotData, MAX_PLOTS_PER_ACCOUNT_PER_TX);                                                                                                                                                 
  const plotChunks = splitEntriesIntoChunksOptimized(splitData, targetEntriesPerChunk);

@pocikerim pocikerim requested a review from fr1jo February 1, 2026 12:40
pocikerim and others added 2 commits February 1, 2026 12:40
🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Contributor

@fr1jo fr1jo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just one minor feedback, but generally this all looks good!

} = require("../../utils/read.js");

// EIP-7987 tx gas limit is 16,777,216 (2^24)
// ~70,600 gas per plot, with 95% safety margin: floor(16,777,216 * 0.95 / 70,600) = 225
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we have a higher safety margin? instead of a 5% safety factor, lets do something like 30%-40% imo

Copy link
Contributor Author

@pocikerim pocikerim Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here: ca816a8

s.accts[account].fields[REPAYMENT_FIELD_ID].plotIndexes.push(podIndex);
s.accts[account].fields[REPAYMENT_FIELD_ID].piIndex[podIndex] = j;
s.accts[account].fields[REPAYMENT_FIELD_ID].piIndex[podIndex] =
s.accts[account].fields[REPAYMENT_FIELD_ID].plotIndexes.length -
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i see you updated the function to be able to be used for accounts that have multiple plots. good job investigating.

can you update this though to cache s.accts[account].fields[REPAYMENT_FIELD_ID].plotIndexes.length? this adds 2100 gas and is unnecessary if we structure this properly

AppStorage storage s = LibAppStorage.diamondStorage();
        for (uint i; i < accountPlots.length; i++) {
            // cache the account and length of the plot indexes array
            address account = accountPlots[i].account;
            uint256 len = s.accts[account].fields[REPAYMENT_FIELD_ID].plotIndexes.length;
            for (uint j; j < accountPlots[i].plots.length; j++) {
                uint256 podIndex = accountPlots[i].plots[j].podIndex;
                uint256 podAmount = accountPlots[i].plots[j].podAmounts;
                s.accts[account].fields[REPAYMENT_FIELD_ID].plots[podIndex] = podAmount;
                s.accts[account].fields[REPAYMENT_FIELD_ID].plotIndexes.push(podIndex);
                s.accts[account].fields[REPAYMENT_FIELD_ID].piIndex[podIndex] =  len + j;
                emit RepaymentPlotAdded(account, podIndex, podAmount);
            }
        }

should work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here: ca816a8

* @notice Mirrors TempRepaymentFieldFacet for gas estimation in batch simulations
* @dev Includes ReentrancyGuard and require check to match real contract gas usage
*/
contract MockTempRepaymentFieldFacet {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is MockTempRepaymentFieldFacet just used to simulate data in scripts/beanstalkShipments/simulateProductionBatches.js?

if so, 1) if the contents is the mock is the same, you can just deploy TempRepaymentFieldFacet.sol and execute the functions directly ( no diamond needed).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep u are right, here: ca816a8

} = require("../../utils/read.js");

// EIP-7987 tx gas limit is 16,777,216 (2^24)
// ~70,600 gas per plot, with 95% safety margin: floor(16,777,216 * 0.95 / 70,600) = 225
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did we get 70,600 gas via the script test? if so, we should include a 2600 gas overhead. In practice, the diamond standard (and proxies in general) have a 2600 gas overhead as a delegatecall has a gas cost to invoke logic from other contracts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's a value from the test output. In the latest batch logic (ca816a8), the minimum number of plots per transaction is 15 (u can see it with running simulateProductionBatches.js). In this case, if I'm not mistaken, the gas overhead per plot would be around 173. I already set the values approximately and didn't account for it since there's a safety margin. But the result from the tests (the average varies depending on the plot count naturally it's higher when there are fewer plots) is around 70-71k.

@@ -0,0 +1,186 @@
const fs = require("fs");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants