Platform: Code4rena
Start Date: 24/10/2023
Pot Size: $36,500 USDC
Total HM: 4
Participants: 147
Period: 6 days
Judge: 0xDjango
Id: 299
League: ETH
Rank: 92/147
Findings: 1
Award: $6.46
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xVolcano
Also found by: 0x11singh99, 0xAadi, 0xAnah, 0xgrbr, 0xhacksmithh, 0xhex, 0xpiken, 0xta, J4X, JCK, K42, Raihan, Rolezn, SAQ, SM3_SS, Sathish9098, SovaSlava, ThreeSigma, Udsen, arjun16, aslanbek, brakelessak, castle_chain, evmboi32, hunter_w3b, lsaudit, naman1778, niser93, nuthan2x, oakcobalt, pavankv, petrichor, phenom80, radev_sw, shamsulhaq123, tabriz, thekmj, unique, yashgoel72, ybansal2403
6.4563 USDC - $6.46
Issue | Instances | Gas saved | |
---|---|---|---|
[G-01] | Pre-calculated constants can be hardcoded | 2 | - |
[G-02] | MAX_COOLDOWN_DURATION can be made constant | 1 | 2100 |
[G-03] | mintedPerBlock and redeemedPerBlock can be implemented in a way that re-uses storage, instead of allocating anew | 2 | 30400 |
[G-03A] | maxMintPerBlock can be storage-optimized with smaller data types | 2 | 4000 |
All of the constants within StakedUSDe
and EthenaMinting
can be hardcoded and commented, instead of calculating on deployment. This saves deployment gas by saving numerous abi.encodePacked()
calls and keccak256()
calls.
Instances:
MAX_COOLDOWN_DURATION
can be made constantIn StakedUSDeV2
, MAX_COOLDOWN_DURATION
is set as a storage variable. However, since it cannot be modified, using a constant will save approx. $2100$ gas on every accesses.
mintedPerBlock
and redeemedPerBlock
can be implemented in a way that re-uses storage, instead of allocating anewNote: We analyze the gas saving for mintedPerBlock
only. redeemedPerBlock
can be applied with the exact same techniques described here.
The mintedPerBlock
mapping stores the amount of minted USDe per block. We first analyze the gas usage:
mint()
operation, the modifier belowMaxMintPerBlock
accesses the storage twice (once for the mapping, once for maxMintPerBlock
), incurring $2100 * 2 = 4200$ gas.Therefore the first redeem for each block will cost at least 25200 gas. We propose a more gas-efficient logic as follow:
mintedPerBlock
, use two storage variables lastMintedBlock
and lastMintedBlockAmount
. They can be packed into a single storage slot.uint32 public lastMintedBlock; uint224 public lastMintedBlockAmount;
lastMintedBlock
(and therefore the storage slot) will only be zero on the first ever mint, we eliminate the need for GSSET operations by writing into non-zero slots only.Modify the belowMaxMintPerBlock()
modifier as follow:
modifier belowMaxMintPerBlock(uint256 mintAmount) { if (block.number != lastMintedBlock) { lastMintedBlock = block.number; lastMintedBlockAmount = 0; } if (lastMintedBlockAmount + mintAmount > maxMintPerBlock) { revert MaxMintPerBlockExceeded(); } _; }
The impact is that, for the first mint of each block, converts one GSSET operation (21000 gas each) into two GSRESET operations (2900 gas each), for 15200 gas saved.
The information on amount minted per past block can be retrieved through events or forking if ever needed.
The exact same logic can be applied to redeem()
, further saving gas for each redeem.
maxMintPerBlock
can be storage-optimized with smaller data typesThis finding builds upon [G-03]. Again, same logic can be applied to redeem()
.
The maximum supply of USDe cannot realistically exceed uint112
data type, given $18$ decimals. Therefore maxMintPerBlock
can be packed together with the two storage variables above, as shown:
uint112 public maxMintPerBlock; uint32 public lastMintedBlock; uint112 public lastMintedBlockAmount;
This reduces the number of storage slots that must be read by one. Converts one Gcoldsload (2100 gas each) to one Gwarmaccess (100 gas). Thereby saving 2000 gas per mint/redeem.
#0 - c4-pre-sort
2023-11-01T15:20:35Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2023-11-10T20:22:45Z
fatherGoose1 marked the issue as grade-b