Platform: Code4rena
Start Date: 04/01/2023
Pot Size: $60,500 USDC
Total HM: 15
Participants: 105
Period: 5 days
Judge: gzeon
Total Solo HM: 1
Id: 200
League: ETH
Rank: 70/105
Findings: 1
Award: $38.76
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xSmartContract
Also found by: 0x1f8b, 0xhacksmithh, Aymen0909, Bnke0x0, IllIllI, Rageur, RaymondFam, Rickard, Rolezn, Secureverse, arialblack14, chaduke, chrisdior4, cthulhu_cult, giovannidisiena, gz627, lukris02, oyc_109, pavankv, privateconstant, shark
38.7634 USDC - $38.76
require
statements that use &&
saves gasInstead of using the &&
operator to check multiple conditions, use multiple require()
statements. This will save approximately 3 gas per &&
.
Here is an example of this issue:
File: ModuleManager.sol
Line 34
require(module != address(0) && module != SENTINEL_MODULES, "BSA101");
In the example above, the &&
could be split up:
require(module != address(0), "BSA101"); require(module != SENTINEL_MODULES, "BSA101");
Here are 2 other instances of this issue:
Strict equalities (<
, >
) cost less gas than non-strict (<=
, >=
). This is because strict equality uses fewer opcodes.
For instance:
File: EntryPoint.sol
Line 213
require(paymasterAndData.length >= 20, "AA93 invalid paymasterAndData");
The require
statement above could use >
instead of >=
to save gas:
require(paymasterAndData.length > 19, "AA93 invalid paymasterAndData");
Here are some more instances of this issue:
StakeManager.sol
Line 41StakeManager.sol
Line 62StakeManager.sol
Line 117EntryPoint.sol
Line 237EntryPoint.sol
Line 397payable
cost less gasFunctions with access control will cost less gas when marked as payable
(assuming the caller has correct permissions). This is because the compiler doesn't need to check for msg.value
, which saves approximately 20 gas per call.
Consider adding payable
to the following functions to save gas:
SmartAccount.sol
Line 449SmartAccount.sol
Line 455SmartAccount.sol
Line 460SmartAccount.sol
Line 465SmartAccount.sol
Line 536if
statementFile: SmartAccount.sol
Line 166 - 176
166 function init(address _owner, address _entryPointAddress, address _handler) public override initializer { 167 require(owner == address(0), "Already initialized"); 168 require(address(_entryPoint) == address(0), "Already initialized"); 169 require(_owner != address(0),"Invalid owner"); 170 require(_entryPointAddress != address(0), "Invalid Entrypoint"); 171 require(_handler != address(0), "Invalid Entrypoint"); 172 owner = _owner; 173 _entryPoint = IEntryPoint(payable(_entryPointAddress)); 174 if (_handler != address(0)) internalSetFallbackHandler(_handler); 175 setupModules(address(0), bytes("")); 176 }
In the function above, the if
statement (line 174) is checking the same thing as line 171, rendering it redundant.
As such, line 174 may be refactored to omit the if
statement:
174 internalSetFallbackHandler(_handler);
x += y
costs more gas than x = x + y
(same for -=
)Using the addition operator instead of plus equals saves around 22 gas
Here are some instances of this issue:
EntryPoint.sol
Line 81EntryPoint.sol
Line 101EntryPoint.sol
Line 135EntryPoint.sol
Line 468Use the unchecked
keyword to avoid unnecessary arithmetic checks and save gas when an underflow/overflow will not happen.
For instance:
File: EntryPoint.sol
Line 100-102
for (uint256 i = 0; i < opasLen; i++) { totalOps += opsPerAggregator[i].userOps.length; }
Since i
is constrained by opasLen
(a uint256
), overflowing will never happen here.
As such, i++
may be unchecked to save gas:
for (uint256 i = 0; i < opasLen;) { totalOps += opsPerAggregator[i].userOps.length; unchecked { i++; } }
uint256()
Casting a number into a uint256()
is unnecessary if the number is already a uint256
.
File: SmartAccount.sol
Line 322
require(uint256(s) >= uint256(1) * 65, "BSA021");
uint256(1)
is redundant as 1
is already a uint256
. Moreover, 1 * 65
is equal to 65
so 1
may be omitted altogether:
require(uint256(s) >= 65, "BSA021");
storage
instead of memory
for structs/arrays saves gasWhen retrieving data from a storage
location, assigning the data to a memory
variable will cause all fields of the struct/array to be read from storage, which incurs a Gcoldsload (2100 gas) for each field of the struct/array. If the fields are read from the new memory
variable, they incur an additional MLOAD rather than a cheap stack read. Instead of declaring the variable with the memory keyword, declaring the variable with the storage
keyword and caching any fields that need to be re-read in stack variables, will be much cheaper, only incurring the Gcoldsload for the fields actually read. The only time it makes sense to read the whole struct/array into a memory
variable, is if the full struct/array is being returned by the function, is being passed to a function that requires memory
, or if the array/struct is being read from another memory
array/struct
File: EntryPoint.sol
(Line 179, Line 229, Line 234, Line 235)
171: MemoryUserOp memory mUserOp = opInfo.mUserOp; 229: UserOpInfo memory outOpInfo; 234: StakeInfo memory paymasterInfo = getStakeInfo(outOpInfo.mUserOp.paymaster); 235: StakeInfo memory senderInfo = getStakeInfo(outOpInfo.mUserOp.sender);
#0 - c4-judge
2023-01-22T16:23:39Z
gzeon-c4 marked the issue as grade-b
#1 - c4-sponsor
2023-02-09T12:32:43Z
livingrockrises marked the issue as sponsor confirmed