Platform: Code4rena
Start Date: 14/03/2024
Pot Size: $49,000 USDC
Total HM: 3
Participants: 51
Period: 7 days
Judge: 3docSec
Id: 350
League: ETH
Rank: 45/51
Findings: 1
Award: $13.69
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: K42
Also found by: 0x11singh99, 0xAnah, Hajime, SAQ, SM3_SS, albahaca, clara, dharma09, hunter_w3b, naman1778, shamsulhaq123, slvDev
13.6948 USDC - $13.69
Initial gas report was saved with forge snapshot
Total gas saved per issue represent Overall gas change
from forge snapshot --diff
.
More details from forge snapshot --diff
can be found in Gas Findings Details.
Issue | Instances | Total Gas Saved | |
---|---|---|---|
[G-01] | Avoid Comparisons of Boolean Expressions to Boolean Literals | 1 | 60 |
[G-02] | Using require() instead of assert() safes gas | 1 | 72 |
[G-03] | Unlimited gas consumption risk due to external call recipients | 1 | - |
[G-04] | Use assembly to check for address(0) | 2 | 116 |
[G-05] | Use assembly for Efficient Event Emission | 3 | 1140 |
[G-06] | internal /private functions only called once can be inlined to save gas | 12 | 240 |
[G-07] | Using bool s for storage incurs overhead | 1 | 100 |
[G-08] | Declare immutable as private to save gas | 1 | 3000 |
[G-09] | Use revert() to gain maximum gas savings | 22 | 1100 |
[G-10] | Using delete on a uint/int variable is cheaper than assigning it to 0 | 1 | 8 |
[G-11] | Optimize Gas by Using Only Named Returns | 22 | 968 |
[G-12] | Enable IR-based code generation | 7 | - |
Direct comparisons of boolean expressions to boolean literals (true or false) can lead to unnecessary gas costs. Instead, use the boolean expression directly or its negation in conditional statements for better gas efficiency.
For instance, replace if (<x> == true)
with if (<x>)
and if (<x> == false)
with if (!<x>)
.
<details> <summary><i>1 issue instances in 1 files:</i></summary>test_createAccountSetsOwnersCorrectly() (gas: -12 (-0.004%)) test_createAccountDeploysToPredeterminedAddress() (gas: -12 (-0.004%)) testDeployDeterministicPassValues() (gas: -12 (-0.004%)) test_CreateAccount_ReturnsPredeterminedAddress_WhenAccountAlreadyExists() (gas: -24 (-0.008%)) Overall gas change: -60 (-0.000%)
</details>File: src/SmartWallet/CoinbaseSmartWalletFactory.sol 53: if (alreadyDeployed == false) {
require()
instead of assert()
safes gasSince assert()
can't contain any message, it is recommended to use require()
without message instead.
Using require()
saves 33 gas per call.
require(false); // 133 gas assert(false); // 166 gas
<details> <summary><i>1 issue instances in 1 files:</i></summary>test_transfersExcess(uint256,uint256,uint256,uint256) (gas: -33 (-0.029%)) test_RevertsIfPostOpFailed(uint256,uint256,uint256) (gas: -39 (-0.040%)) Overall gas change: -72 (-0.000%)
</details>File: src/MagicSpend/MagicSpend.sol 150: assert(mode != PostOpMode.postOpReverted)
When calling an external function without specifying a gas limit , the called contract may consume all the remaining gas, causing the tx to be reverted. To mitigate this, it is recommended to explicitly set a gas limit when making low level external calls.
<details> <summary><i>1 issue instances in 1 files:</i></summary></details>File: src/SmartWallet/CoinbaseSmartWallet.sol 273: (bool success, bytes memory result) = target.call{value: value}(data);
address(0)
The usage of inline assembly to check if variable is the zero can save gas compared to traditional require
or if
statement checks.
The assembly check uses the extcodesize
operation which is generally cheaper in terms of gas.
More information can be found here.
<details> <summary><i>2 issue instances in 1 files:</i></summary></details>File: src/MagicSpend/MagicSpend.sol 121: if (withdrawRequest.asset != address(0)) { revert UnsupportedPaymasterAsset(withdrawRequest.asset); 335: if (asset == address(0)) { SafeTransferLib.safeTransferETH(to, amount);
assembly
for Efficient Event EmissionTo efficiently emit events, consider utilizing assembly by making use of scratch space and the free memory pointer. This approach can potentially avoid the costs associated with memory expansion.
However, it's crucial to cache and restore the free memory pointer for safe optimization. Good examples of such practices can be found in well-optimized Solady's codebases. Please review your code and consider the potential gas savings of this approach.
<details> <summary><i>3 issue instances in 2 files:</i></summary>File: src/SmartWallet/MultiOwnable.sol 109: emit RemoveOwner(index, owner) 195: emit AddOwner(index, owner)
</details>File: src/MagicSpend/MagicSpend.sol 323: emit MagicSpendWithdrawal(account, withdrawRequest.asset, withdrawRequest.amount, withdrawRequest.nonce)
internal
/private
functions only called once can be inlined to save gasinternal
functions that are only called once should be inlined to save gas.
Not inlining such functions costs an extra 20 to 40 gas due to the additional JUMP
instructions and stack operations required for function calls.
File: src/SmartWallet/MultiOwnable.sol /// @audit function `_checkOwner()` called only once 201: function _checkOwner() internal view virtual {
File: src/SmartWallet/ERC1271.sol /// @audit function `_eip712Hash()` called only once 121: function _eip712Hash(bytes32 hash) internal view virtual returns (bytes32) { /// @audit function `_hashStruct()` called only once 133: function _hashStruct(bytes32 hash) internal view virtual returns (bytes32) {
File: src/SmartWallet/CoinbaseSmartWallet.sol /// @audit function `_validateSignature()` called only once 291: function _validateSignature(bytes32 message, bytes calldata signature) internal view virtual override returns (bool) {
File: src/FreshCryptoLib/FCL.sol /// @audit function `ecAff_isOnCurve()` called only once 56: function ecAff_isOnCurve(uint256 x, uint256 y) internal pure returns (bool) { /// @audit function `FCL_nModInv()` called only once 72: function FCL_nModInv(uint256 u) internal view returns (uint256 result) { /// @audit function `ecZZ_mulmuladd_S_asm()` called only once 95: function ecZZ_mulmuladd_S_asm( uint256 Q0, uint256 Q1, //affine rep for input point Q uint256 scalar_u, uint256 scalar_v ) internal view returns (uint256 X) { /// @audit function `ecAff_add()` called only once 252: function ecAff_add(uint256 x0, uint256 y0, uint256 x1, uint256 y1) internal view returns (uint256, uint256) { /// @audit function `ecZZ_SetAff()` called only once 279: function ecZZ_SetAff(uint256 x, uint256 y, uint256 zz, uint256 zzz) internal view returns (uint256 x1, uint256 y1) { /// @audit function `ecZZ_Dbl()` called only once 296: function ecZZ_Dbl(uint256 x, uint256 y, uint256 zz, uint256 zzz) internal pure returns (uint256 P0, uint256 P1, uint256 P2, uint256 P3) { /// @audit function `ecZZ_AddN()` called only once 322: function ecZZ_AddN(uint256 x1, uint256 y1, uint256 zz1, uint256 zzz1, uint256 x2, uint256 y2) internal pure returns (uint256 P0, uint256 P1, uint256 P2, uint256 P3) { /// @audit function `FCL_pModInv()` called only once 352: function FCL_pModInv(uint256 u) internal view returns (uint256 result) {
56 | 72 | 95 | 252 | 279 | 296 | 322 | 352
</details>bool
s for storage incurs overheadUtilizing booleans for storage is less gas-efficient compared to using types that consume a full word like uint256. Every write operation on a boolean necessitates an extra SLOAD operation to read the slot's current value, modify the boolean bits, and then write back. This additional step is the compiler's measure against contract upgrades and pointer aliasing.
To enhance gas efficiency, consider using uint256(0)
for false and uint256(1)
for true, bypassing the extra Gwarmaccess (100 gas) incurred by the SLOAD.
</details>File: src/MagicSpend/MagicSpend.sol 37: mapping(uint256 nonce => mapping(address user => bool used)) internal _nonceUsed;
immutable
as private
to save gasUsing private
instead of public
for immutables saves gas.
The compiler doesn't need to create non-payable getter functions for deployment calldata, store the bytes of the value outside of where it's used, or add another entry to the method ID table, saving 3406-3606 gas in deployment.
<details> <summary><i>1 issue instances in 1 files:</i></summary></details>File: src/SmartWallet/CoinbaseSmartWalletFactory.sol 15: address public immutable implementation;
revert()
to gain maximum gas savingsIf you dont need Error messages, or you want gain maximum gas savings - revert()
is a cheapest way to revert transaction in terms of gas.
<details> <summary><i>22 issue instances in 4 files:</i></summary>revert(); // 117 gas require(false); // 132 gas revert CustomError(); // 157 gas assert(false); // 164 gas revert("Custom Error"); // 406 gas require(false, "Custom Error"); // 421 gas
File: src/SmartWallet/MultiOwnable.sol 104: revert NoOwnerAtIndex(index) 165: revert InvalidOwnerBytesLength(owners[i]) 169: revert InvalidEthereumAddressOwner(owners[i]) 190: revert AlreadyOwner(owner) 206: revert Unauthorized()
File: src/SmartWallet/CoinbaseSmartWalletFactory.sol 45: revert OwnerRequired()
File: src/SmartWallet/CoinbaseSmartWallet.sol 67: revert Unauthorized() 116: revert Initialized() 151: revert InvalidNonceKey(key) 155: revert InvalidNonceKey(key) 183: revert SelectorNotAllowed(selector) 305: revert InvalidEthereumAddressOwner(ownerBytes) 324: revert InvalidOwnerBytesLength(ownerBytes)
67 | 116 | 151 | 155 | 183 | 305 | 324
File: src/MagicSpend/MagicSpend.sol 150: assert(mode != PostOpMode.postOpReverted) 94: revert Unauthorized() 118: revert RequestLessThanGasMaxCost(withdrawAmount, maxCost) 122: revert UnsupportedPaymasterAsset(withdrawRequest.asset) 134: revert InsufficientBalance(withdrawAmount, address(this).balance) 172: revert NoExcess() 185: revert InvalidSignature() 189: revert Expired() 317: revert InvalidNonce(withdrawRequest.nonce)
150 | 94 | 118 | 122 | 134 | 172 | 185 | 189 | 317
</details>delete
on a uint/int
variable is cheaper than assigning it to 0
<details> <summary><i>1 issue instances in 1 files:</i></summary>function test() external { delete foo; // 5148 gas foo = 0; // 5156 gas }
</details>File: src/FreshCryptoLib/FCL.sol 116: scalar_v = 0
Consider using receive()
function instead of a specific deposit()
(or similar) function.
If there are several functions in the contract that can receive Ether, it is recommended to use receive()
for the most frequently used function.
function deposit() external payable { // 5401 gas // your logic } ### [G-16] Optimize Ether Transfers with `receive()` Function receive() external payable { // 5356 gas // your logic }
The receive()
or fallback()
function can handle incoming Ether transfers directly, providing more gas-efficient way to manage deposits.
</details>File: src/MagicSpend/MagicSpend.sol 212: function entryPointDeposit(uint256 amount) external payable onlyOwner 232: function entryPointAddStake(uint256 amount, uint32 unstakeDelaySeconds) external payable onlyOwner
The Solidity compiler can generate more efficient bytecode when using named returns. It's recommended to replace anonymous returns with named returns for potential gas savings.
Example:
<details> <summary><i>22 issue instances in 6 files:</i></summary>/// 985 gas cost function add(uint256 x, uint256 y) public pure returns (uint256) { return x + y; } /// 941 gas cost function addNamed(uint256 x, uint256 y) public pure returns (uint256 res) { res = x + y; }
File: src/SmartWallet/MultiOwnable.sol 117: function isOwnerAddress(address account) public view virtual returns (bool) { 127: function isOwnerPublicKey(bytes32 x, bytes32 y) public view virtual returns (bool) { 136: function isOwnerBytes(bytes memory account) public view virtual returns (bool) { 145: function ownerAtIndex(uint256 index) public view virtual returns (bytes memory) { 152: function nextOwnerIndex() public view virtual returns (uint256) {
File: src/SmartWallet/ERC1271.sol 90: function replaySafeHash(bytes32 hash) public view virtual returns (bytes32) { 100: function domainSeparator() public view returns (bytes32) { 121: function _eip712Hash(bytes32 hash) internal view virtual returns (bytes32) { 133: function _hashStruct(bytes32 hash) internal view virtual returns (bytes32) { 155: function _validateSignature(bytes32 message, bytes calldata signature) internal view virtual returns (bool);
File: src/SmartWallet/CoinbaseSmartWallet.sol 217: function entryPoint() public view virtual returns (address) { 252: function canSkipChainIdValidation(bytes4 functionSelector) public pure returns (bool) { 291: function _validateSignature(bytes32 message, bytes calldata signature) internal view virtual override returns (bool) { 333: function _domainNameAndVersion() internal pure override(ERC1271) returns (string memory, string memory) {
File: src/WebAuthnSol/WebAuthn.sol 104: function verify(bytes memory challenge, bool requireUV, WebAuthnAuth memory webAuthnAuth, uint256 x, uint256 y) internal view returns (bool) {
File: src/FreshCryptoLib/FCL.sol 27: function ecdsa_verify(bytes32 message, uint256 r, uint256 s, uint256 Qx, uint256 Qy) internal view returns (bool) { 56: function ecAff_isOnCurve(uint256 x, uint256 y) internal pure returns (bool) { 251: function ecAff_add(uint256 x0, uint256 y0, uint256 x1, uint256 y1) internal view returns (uint256, uint256) {
</details>File: src/MagicSpend/MagicSpend.sol 260: function isValidWithdrawSignature(address account, WithdrawRequest memory withdrawRequest) public view returns (bool) { 279: function getHash(address account, WithdrawRequest memory withdrawRequest) public view returns (bytes32) { 299: function nonceUsed(address account, uint256 nonce) external view returns (bool) { 304: function entryPoint() public pure returns (address) {
The --via-ir
command line option activates the IR-based code generator in Solidity, which is designed to enable powerful optimization passes that can span across functions. The end result may be a contract that requires less gas to execute its functions.
We recommend you enable this feature, run tests, and benchmark the gas usage of your contract to evaluate if it leads to any tangible gas savings. Experimenting with this feature could lead to a more gas-efficient contract.
<details> <summary><i>7 issue instances in 1 files:</i></summary></details>File: src/SmartWallet/MultiOwnable.sol File: src/SmartWallet/ERC1271.sol File: src/SmartWallet/CoinbaseSmartWalletFactory.sol File: src/SmartWallet/CoinbaseSmartWallet.sol File: src/WebAuthnSol/WebAuthn.sol File: src/FreshCryptoLib/FCL.sol File: src/MagicSpend/MagicSpend.sol
#0 - raymondfam
2024-03-22T21:44:37Z
12 generic G.
#1 - c4-pre-sort
2024-03-22T21:44:41Z
raymondfam marked the issue as sufficient quality report
#2 - c4-judge
2024-03-27T13:17:52Z
3docSec marked the issue as grade-b