-
Notifications
You must be signed in to change notification settings - Fork 12
Reduce uint256 fields to smaller integer types for gas and storage savings #85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
Hey @pali101 Thanks for the PR ! But we're not looking to make deep changes related to gas savings for now as we're still in the mode of adding features to this contract and flushing out bugs/additional features. I would recommend taking a look at open issues to see if something catches your eye. |
|
@pali101 Actually, this is good. Please can you rebase the PR on top of main and I can then review/merge this ? |
3c7c597 to
5aca14d
Compare
…d repack Rail struct Improve storage packing and reduce per-rail gas costs.
…uint256 to unit16) in public interfaces
Affected Signatures:
- setPayeeMaxCommission(address token, uint16 maxBps)
- createRail(address token, address from, address to, address arbiter, uint16 commissionRateBps)
- modifyRailLockup(uint256 railId, uint64 period, uint256 lockupFixed)
- settleRail(uint256 railId, uint64 untilEpoch)
- getRail(uint256 railId) -> returns RailView {
// narrowed:
uint64 lockupPeriod;
uint64 settledUpTo;
uint64 endEpoch;
uint16 commissionRateBps;
// other fields unchanged
}
- getRailsForPayerAndToken(address payer, address token) → returns RailInfo[] { uint64 endEpoch; other fields unchanged }
- getRailsForPayeeAndToken(address payee, address token) → returns RailInfo[] { uint64 endEpoch; other fields unchanged }
- RateChangeQueue.enqueue(Queue storage, uint256 rate, uint64 untilEpoch)
- removed setPayeeMaxCommission function
… introduce min64 helper function - Add `min64(uint64 a, uint64 b)` to unambiguously compute the smaller of two 64-bit values. - Update settlement logic to call `min64` for all uint64 comparisons. - Ensure explicit `uint64(...)` casts when writing block numbers or division results back into 64-bit state fields.
- PaymentsTestHelpers: - getAccountData() now unpacks lockupLastSettledAt as uint64 and return Payments.Account matching new signature - assertAccountState expectedLastSettled parameter changed to uint64 - setupRailWithParameters accepts lockupPeriod as uint64 - RailSettlementHelpers: - SettlementResult.settledUpto field changed to uint64 - settleRailAndVerify and terminateAndSettleRail signatures updated to use uint64 - Added explicit `uint64(block.number)` casts where needed
…eUpto - Change `arbitratePayment` signature to `uint64 fromEpoch, uint64 toEpoch` per IArbiter - Convert `customUpto` storage and `setCustomValues` param to `uint64` - Compute and return 64-bit `settleUpto` values (e.g. cast reducedEndEpoch)
Update the following test files to use uint64 for epoch/block parameters and uint16 for BPS fields: - AccountLockupSettlement.t.sol - Fees.t.sol - RailGetters.t.sol - RateChangeQueue.t.sol - AccountManagement.t.sol - OperatorApproval.t.sol - RailSettlement.t.sol
…ix 'stack too deep'
5aca14d to
1591fd7
Compare
|
@aarshkshah1992 I've rebased it on top of |
silent-cipher
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please conduct a comprehensive audit of all uint64/uint256 type usage throughout the contract
| struct Rail { | ||
| // slot 0: 20 B + 8 B = 28 B used (4 B free) | ||
| address token; | ||
| uint64 lockupPeriod; | ||
|
|
||
| // slot 1: 20 B + 8 B = 28 B used | ||
| address from; | ||
| // epoch up to and including which this rail has been settled | ||
| uint64 settledUpTo; | ||
|
|
||
| // slot 2: 20 B + 8 B = 28 B used | ||
| address to; | ||
| uint64 endEpoch; // Final epoch up to which the rail can be settled (0 if not terminated) | ||
|
|
||
| // slot 3: 20 B + 2 B = 22 B used | ||
| address operator; | ||
| // Operator commission rate in basis points (e.g., 100 BPS = 1%) | ||
| uint16 commissionRateBps; | ||
|
|
||
| // slot 4: 20 B used | ||
| address arbiter; | ||
|
|
||
| // slot 5: 32 B used | ||
| uint256 paymentRate; | ||
| uint256 lockupPeriod; | ||
|
|
||
| // slot 6: 32 B used | ||
| uint256 lockupFixed; | ||
| // epoch up to and including which this rail has been settled | ||
| uint256 settledUpTo; | ||
|
|
||
| // RateChangeQueue.Queue is 3×32 B (mapping + head + tail) slots 7, 8, 9 | ||
| RateChangeQueue.Queue rateChangeQueue; | ||
| uint256 endEpoch; // Final epoch up to which the rail can be settled (0 if not terminated) | ||
| // Operator commission rate in basis points (e.g., 100 BPS = 1%) | ||
| uint256 commissionRateBps; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typical user may find these slot packing details overwhelming and may go out of sync easily
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the feedback! The slot comments are just for internal developer reference to understand the storage layout and optimize packing. While they may need manual updates, actual access to variables doesn’t change - just their arrangement for better packing and gas efficiency. They don’t impact typical users or contract behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These comments are useful but are too much clutter for this place in the code. Rail struct comments should be exclusively about the semantic values of the fields. I think the correct place for this is either in a top level struct comment where you outline the complete packing behavior in one place OR in a small design document / in the README which is linked to in a top level struct comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A benefit of doing it this way is that the packing design will be more readable. I'm having trouble parsing the meaning of the numbers here and understanding how much space is actually being saved. An ascii diagram would probably be worth it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because the contract is a proxy, notation of the storage slots is an important part of the upkeep of the contract. The notes are important therefore. It would also be advantageous to declare all storage in a single file, to reduce the likelihood of underhanded or accidental slot shiftings between upgrades.
| // If there is no arbiter, settle the rail immediately | ||
| if (rail.arbiter == address(0)) { | ||
| (, , , , uint256 settledUpto, ) = settleRail(railId, block.number); | ||
| (, , , , uint256 settledUpto, ) = settleRail(railId, uint64(block.number)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type inconsistency throughout:
rail.settledUpToisuint64but compared withuint256 block.numbersettledUptoshould beuint64notuint256- Mixed type comparisons without explicit casting
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was an intentional choice to avoid explicit type casting, which can introduce additional gas costs. Solidity safely allows comparisons between uint64 and uint256 as long as values are within range.
For example, comparing uint64(1) with uint256(1) evaluates correctly, and even values like uint64(123456) compared to uint256(123456) yield accurate results. Since block.number is well within the uint64 range under current and foreseeable network conditions, the logic remains safe and correct.
Avoiding the cast here is a small but deliberate gas optimization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand the comparison response but shouldne't settledUpto here be declared as uint64?
|
@pali101 This is not a priority for now and so wont be merging it for a while as we're still figuring out the important work items for the payments contract and will have to prioritize accordingly. Just keeping it open for now |
|
I'm moving this PR back to draft to indicate that it's not expected to land anytime soon, given the upcoming code freeze and external audit. Depending on feedback from the audit or other future input, gas and storage savings work might become a priority again. However, at this point, I don't anticipate us moving forward with it in Milestone 2. |
| struct Rail { | ||
| // slot 0: 20 B + 8 B = 28 B used (4 B free) | ||
| address token; | ||
| uint64 lockupPeriod; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider uint96 instead of uint64 in combination with address160 since you aren't using the other32
|
Another strategy for compressing large structs is to compress most of their information with a hash. The usual way to do this is to emit the uncompressed information in an event log, and have the end user resupply it later as calldata, which can then be verified by comparison to the stored hash. |
Closes #75
Summary
Most struct fields were
uint256even though their values never approach 2256 - 1. This PR:uint64for epochs anduint16for bps.Changes
RailstructlockupPeriod-uint64settledUpTo-uint64endEpoch-uint64commissionRateBps-uint16AccountstructlockupLastSettledAt-uint64PayeeCommissionLimitstructmaxbps-uint16Reordered all small width fields to maximize slot packing.
Gas and size impact
We ran
forge test --gas-reporton Foundry and compared the before/after outputs (see attached .txt files). Overall, core rail-operations (e.g.createRail,getRail,modifyRailLockup,terminateRail) saw 13–31% per-call gas savings on average and median, at the cost of a modest 0.75% increase in overall deployment cost. See details below:Optimized gas report (struct-packing)
Baseline gas report (pre-optimization)
Testing
All 79 existing Solidity tests pass unchanged.
cc @Stebalien - I've implemented the struct‐packing optimizations per #75, would love your review.