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: 7/105
Findings: 3
Award: $1,208.86
🌟 Selected for report: 2
🚀 Solo Findings: 0
🌟 Selected for report: 0x52
Also found by: 0xSmartContract, Deivitto, Diana, IllIllI, Koolex, Rolezn, SleepingBugs, V_B, adriro, betweenETHlines, cryptostellar5, oyc_109, peanuts
44.8261 USDC - $44.83
Judge has assessed an item in Issue #157 as 2 risk. The relevant finding follows:
[L-08] No Storage Gap for BaseSmartAccount and ModuleManager
#0 - c4-judge
2023-02-12T16:25:33Z
gzeon-c4 marked the issue as duplicate of #261
#1 - c4-judge
2023-02-12T16:25:52Z
gzeon-c4 marked the issue as satisfactory
🌟 Selected for report: 0xSmartContract
Also found by: 0x1f8b, 0xAgro, 0xdeadbeef0x, 0xhacksmithh, 2997ms, Atarpara, Bnke0x0, Diana, HE1M, IllIllI, Josiah, Kalzak, Lirios, MalfurionWhitehat, MyFDsYours, Raiders, RaymondFam, Rolezn, SaharDevep, Sathish9098, Udsen, Viktor_Cortess, adriro, ast3ros, betweenETHlines, btk, chaduke, chrisdior4, cryptostellar5, csanuragjain, giovannidisiena, gz627, hl_, horsefacts, joestakey, juancito, ladboy233, lukris02, nadin, oyc_109, pauliax, peanuts, prady, sorrynotsorry, zaskoh
469.3187 USDC - $469.32
Number | Issues Details | Context |
---|---|---|
[L-01] | Prevent division by 0 | 1 |
[L-02] | Use of EIP 4337, which is likely to change, not recommended for general use or application | 1 |
[L-03] | Consider using OpenZeppelin's SafeCast library to prevent unexpected overflows when casting from uint256 | 1 |
[L-04] | Gas griefing/theft is possible on unsafe external call | 8 |
[L-05] | Front running attacks by the onlyOwner | 1 |
[L-06] | A single point of failure | 14 |
[L-07] | Loss of precision due to rounding | 1 |
[L-08] | No Storage Gap for BaseSmartAccount and ModuleManager | 2 |
[L-09] | Missing Event for critical parameters init and change | 1 |
[L-10] | Use 2StepSetOwner instead of setOwner | 1 |
[L-11] | init() function can be called by anybody | 1 |
[L-12] | The minimum transaction value of 21,000 gas may change in the future | 1 |
Total 12 issues
Number | Issues Details | Context |
---|---|---|
[N-01] | Insufficient coverage | 1 |
[N-02] | Unused function parameter and local variable | 2 |
[N-03] | Initial value check is missing in Set Functions | 3 |
[N-04] | NatSpec comments should be increased in contracts | All Contracts |
[N-05] | Function writing that does not comply with the Solidity Style Guide | All Contracts |
[N-06] | Add a timelock to critical functions | 1 |
[N-07] | For modern and more readable code; update import usages | 116 |
[N-08] | Include return parameters in NatSpec comments | All Contracts |
[N-09] | Long lines are not suitable for the ‘Solidity Style Guide’ | 9 |
[N-10] | Need Fuzzing test | 23 |
[N-11] | Test environment comments and codes should not be in the main version | 1 |
[N-12] | Use of bytes.concat() instead of abi.encodePacked() | 5 |
[N-13] | For functions, follow Solidity standard naming conventions (internal function style rule) | 13 |
[N-14] | Omissions in Events | 1 |
[N-15] | Open TODOs | 1 |
[N-16] | Mark visibility of init(...) functions as external | 1 |
[N-17] | Use underscores for number literals | 2 |
[N-18] | Empty blocks should be removed or Emit something | 2 |
[N-19] | Use require instead of assert | 2 |
[N-20] | Implement some type of version counter that will be incremented automatically for contract upgrades | 1 |
[N-21] | Tokens accidentally sent to the contract cannot be recovered | 1 |
[N-22] | Use a single file for all system-wide constants | 10 |
[N-23] | Assembly Codes Specific – Should Have Comments | 72 |
Total 23 issues
Number | Suggestion Details |
---|---|
[S-01] | Project Upgrade and Stop Scenario should be |
[S-02] | Use descriptive names for Contracts and Libraries |
Total2 suggestions
On several locations in the code precautions are not being taken for not dividing by 0, this will revert the code. These functions can be calledd with 0 value in the input, this value is not checked for being bigger than 0, that means in some scenarios this can potentially trigger a division by zero.
2 results - 1 file contracts/smart-contract-wallet/SmartAccount.sol: 264: payment = (gasUsed + baseGas) * (gasPrice) / (tokenGasPriceFactor); 288: payment = (gasUsed + baseGas) * (gasPrice) / (tokenGasPriceFactor);
contracts/smart-contract-wallet/SmartAccount.sol: 246 247: function handlePayment( 248: uint256 gasUsed, 249: uint256 baseGas, 250: uint256 gasPrice, 251: uint256 tokenGasPriceFactor, 252: address gasToken, 253: address payable refundReceiver 254: ) private nonReentrant returns (uint256 payment) { 255: // uint256 startGas = gasleft(); 256: // solhint-disable-next-line avoid-tx-origin 257: address payable receiver = refundReceiver == address(0) ? payable(tx.origin) : refundReceiver; 258: if (gasToken == address(0)) { 259: // For ETH we will only adjust the gas price to not be higher than the actual used gas price 260: payment = (gasUsed + baseGas) * (gasPrice < tx.gasprice ? gasPrice : tx.gasprice); 261: (bool success,) = receiver.call{value: payment}(""); 262: require(success, "BSA011"); 263: } else { 264: payment = (gasUsed + baseGas) * (gasPrice) / (tokenGasPriceFactor); 265: require(transferToken(gasToken, receiver, payment), "BSA012"); 266: } 267: // uint256 requiredGas = startGas - gasleft(); 268: //console.log("hp %s", requiredGas); 269: } 270: 271: function handlePaymentRevert( 272: uint256 gasUsed, 273: uint256 baseGas, 274: uint256 gasPrice, 275: uint256 tokenGasPriceFactor, 276: address gasToken, 277: address payable refundReceiver 278: ) external returns (uint256 payment) { 279: uint256 startGas = gasleft(); 280: // solhint-disable-next-line avoid-tx-origin 281: address payable receiver = refundReceiver == address(0) ? payable(tx.origin) : refundReceiver; 282: if (gasToken == address(0)) { 283: // For ETH we will only adjust the gas price to not be higher than the actual used gas price 284: payment = (gasUsed + baseGas) * (gasPrice < tx.gasprice ? gasPrice : tx.gasprice); 285: (bool success,) = receiver.call{value: payment}(""); 286: require(success, "BSA011"); 287: } else { 288: payment = (gasUsed + baseGas) * (gasPrice) / (tokenGasPriceFactor); 289: require(transferToken(gasToken, receiver, payment), "BSA012"); 290: } 291: uint256 requiredGas = startGas - gasleft(); 292: //console.log("hpr %s", requiredGas); 293: // Convert response to string and return via error message 294: revert(string(abi.encodePacked(requiredGas))); 295: }
contracts/smart-contract-wallet/SmartAccountFactory.sol: 28 * @param _owner EOA signatory of the wallet 29: * @param _entryPoint AA 4337 entry point address 30 * @param _handler fallback handler address 49 * @param _owner EOA signatory of the wallet 50: * @param _entryPoint AA 4337 entry point address 51 * @param _handler fallback handler address contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol: 1 /** 2: ** Account-Abstraction (EIP-4337) singleton EntryPoint implementation. 3 ** Only one instance required on each chain. contracts/smart-contract-wallet/aa-4337/interfaces/IEntryPoint.sol: 1 /** 2: ** Account-Abstraction (EIP-4337) singleton EntryPoint implementation. 3 ** Only one instance required on each chain. contracts/smart-contract-wallet/aa-4337/samples/DepositPaymaster.sol: 21 * paymasterAndData holds the paymaster address followed by the token address to use. 22: * @notice This paymaster will be rejected by the standard rules of EIP4337, as it uses an external oracle. 23 * (the standard rules ban accessing data of an external contract)
An account abstraction proposal which completely avoids the need for consensus-layer protocol changes.
However, this EIP has not been finalized yet, there is a warning situation that is not of general use.
If it is desired to be used, it is recommended to perform high-level security controls such as Formal Verification.
https://eips.ethereum.org/EIPS/eip-4337
contracts/smart-contract-wallet/aa-4337/core/StakeManager.sol: 114 */ 115: function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external { 116: DepositInfo storage info = deposits[msg.sender]; 117: require(withdrawAmount <= info.deposit, "Withdraw amount too large"); 118: info.deposit = uint112(info.deposit - withdrawAmount); 119: emit Withdrawn(msg.sender, withdrawAddress, withdrawAmount); 120: (bool success,) = withdrawAddress.call{value : withdrawAmount}(""); 121: require(success, "failed to withdraw"); 122: } 123 }
In the StakeManager contract, the withdrawTo
function takes an argument withdrawAmount
of type uint256
Now, in the function, the value of withdrawAmount
is downcasted to uint112
Recommended Mitigation Steps: Consider using OpenZeppelin's SafeCast library to prevent unexpected overflows when casting from uint256.
return
data (bool success,)
has to be stored due to EVM architecture, if in a usage like below, 'out' and 'outsize' values are given (0,0) . Thus, This storage disappears and may come from external contracts a possible Gas griefing/theft problem is avoided
https://twitter.com/pashovkrum/status/1607024043718316032?t=xs30iD6ORWtE2bTTYsCFIQ&s=19
There are 8 instances of the topic.
contracts\smart-contract-wallet\SmartAccount.sol#l451 449 function transfer(address payable dest, uint amount) external nonReentrant onlyOwner { - 451: (bool success,) = dest.call{value:amount}(""); + assembly { + success := call(gas(), dest, amount, 0, 0) + } + 452 require(success,"transfer failed"); 453 } 454
contracts\smart-contract-wallet\SmartAccount.sol: 247 function handlePayment( 261: (bool success,) = receiver.call{value: payment}(""); 262 require(success, "BSA011");
271 function handlePaymentRevert( 285: (bool success,) = receiver.call{value: payment}(""); 286 require(success, "BSA011");
contracts\smart-contract-wallet\SmartAccount.sol: 525 function addDeposit() public payable { 526 527: (bool req,) = address(entryPoint()).call{value : msg.value}(""); 528 require(req); 529 }
contracts\smart-contract-wallet\aa-4337\core\EntryPoint.sol: 35 function _compensate(address payable beneficiary, uint256 amount) internal { 36 require(beneficiary != address(0), "AA90 invalid beneficiary"); 37: (bool success,) = beneficiary.call{value : amount}(""); 38 require(success, "AA91 failed send to beneficiary"); 39 }
contracts\smart-contract-wallet\aa-4337\core\StakeManager.sol: 96 function withdrawStake(address payable withdrawAddress) external { 106: (bool success,) = withdrawAddress.call{value : stake}(""); 107 require(success, "failed to withdraw stake"); 108 }
contracts\smart-contract-wallet\aa-4337\core\StakeManager.sol: 115 function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external { 120: (bool success,) = withdrawAddress.call{value : withdrawAmount}(""); 121 require(success, "failed to withdraw"); 122 }
contracts/smart-contract-wallet/BaseSmartAccount.sol: 106 function _payPrefund(uint256 missingAccountFunds) internal virtual { 108: (bool success,) = payable(msg.sender).call{value : missingAccountFunds, gas : type(uint256).max}(""); 109 (success);
onlyOwner
verifyingSigner
value is not a constant value and can be changed with setSigner
function, before a function using verifyingSigner
state variable value in the project, setSigner
function can be triggered by onlyOwner
and operations can be blocked
contracts/smart-contract-wallet/paymasters/verifying/singleton/VerifyingSingletonPaymaster.sol: 64 */ 65: function setSigner( address _newVerifyingSigner) external onlyOwner{ 66: require(_newVerifyingSigner != address(0), "VerifyingPaymaster: new signer can not be zero address"); 67: verifyingSigner = _newVerifyingSigner; 68: }
Use a timelock to avoid instant changes of the parameters.
The onlyOwner
role has a single point of failure and onlyOwner
can use critical a few functions.
Even if protocol admins/developers are not malicious there is still a chance for Owner keys to be stolen. In such a case, the attacker can cause serious damage to the project due to important functions. In such a case, users who have invested in project will suffer high financial losses.
onlyOwner
functions;
14 results - 3 files contracts/smart-contract-wallet/SmartAccount.sol: 72: // onlyOwner 76: modifier onlyOwner { 81: // onlyOwner OR self 449: function transfer(address payable dest, uint amount) external nonReentrant onlyOwner { 455: function pullTokens(address token, address dest, uint256 amount) external onlyOwner { 460: function execute(address dest, uint value, bytes calldata func) external onlyOwner{ 465: function executeBatch(address[] calldata dest, bytes[] calldata func) external onlyOwner{ 536: function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public onlyOwner { contracts/smart-contract-wallet/paymasters/BasePaymaster.sol: 24: function setEntryPoint(IEntryPoint _entryPoint) public onlyOwner { 67: function withdrawTo(address payable withdrawAddress, uint256 amount) public virtual onlyOwner { 75: function addStake(uint32 unstakeDelaySec) external payable onlyOwner { 90: function unlockStake() external onlyOwner { 99: function withdrawStake(address payable withdrawAddress) external onlyOwner { contracts/smart-contract-wallet/paymasters/verifying/singleton/VerifyingSingletonPaymaster.sol: 65: function setSigner( address _newVerifyingSigner) external onlyOwner{
This increases the risk of A single point of failure
Manuel Code Review
Add a time lock to critical functions. Admin-only functions that change critical parameters should emit events and have timelocks.
Events allow capturing the changed parameters so that off-chain tools/interfaces can register such changes with timelocks that allow users to evaluate them and consider if they would like to engage/exit based on how they perceive the changes as affecting the trustworthiness of the protocol or profitability of the implemented financial services.
Also detail them in documentation and NatSpec comments
Add scalars so roundings are negligible
2 results - 1 file contracts/smart-contract-wallet/SmartAccount.sol: 264: payment = (gasUsed + baseGas) * (gasPrice) / (tokenGasPriceFactor); 288: payment = (gasUsed + baseGas) * (gasPrice) / (tokenGasPriceFactor);
BaseSmartAccount
and ModuleManager
BaseSmartAccount.sol#L33 ModuleManager.sol#L9
For upgradeable contracts, inheriting contracts may introduce new variables. In order to be able to add new variables to the upgradeable contract without causing storage collisions, a storage gap should be added to the upgradeable contract.
If no storage gap is added, when the upgradable contract introduces new variables, it may override the variables in the inheriting contract.
Storage gaps are a convention for reserving storage slots in a base contract, allowing future versions of that contract to use up those slots without affecting the storage layout of child contracts. To create a storage gap, declare a fixed-size array in the base contract with an initial number of slots. This can be an array of uint256 so that each element reserves a 32 byte slot. Use the naming convention __gap so that OpenZeppelin Upgrades will recognize the gap:
Classification for a similar problem: https://code4rena.com/reports/2022-05-alchemix/#m-05-no-storage-gap-for-upgradeable-contract-might-lead-to-storage-slot-collision
contract Base { uint256 base1; uint256[49] __gap; } contract Child is Base { uint256 child; }
Openzeppelin Storage Gaps notification:
Storage Gaps This makes the storage layouts incompatible, as explained in Writing Upgradeable Contracts. The size of the __gap array is calculated so that the amount of storage used by a contract always adds up to the same number (in this case 50 storage slots).
Consider adding a storage gap at the end of the upgradeable abstract contract
uint256[50] private __gap;
Context:
function init(address _owner, address _entryPointAddress, address _handler) public override initializer { require(owner == address(0), "Already initialized"); require(address(_entryPoint) == address(0), "Already initialized"); require(_owner != address(0),"Invalid owner"); require(_entryPointAddress != address(0), "Invalid Entrypoint"); require(_handler != address(0), "Invalid Entrypoint"); owner = _owner; _entryPoint = IEntryPoint(payable(_entryPointAddress)); if (_handler != address(0)) internalSetFallbackHandler(_handler); setupModules(address(0), bytes("")); }
Description: Events help non-contract tools to track changes, and events prevent users from being surprised by changes
Recommendation: Add Event-Emit
2StepSetOwner
instead of setOwner
1 result - 1 file contracts/smart-contract-wallet/SmartAccount.sol: 109: function setOwner(address _newOwner) external mixedAuth { 110: require(_newOwner != address(0), "Smart Account:: new Signatory address cannot be zero"); 111: address oldOwner = owner; 112: owner = _newOwner; 113: emit EOAChanged(address(this), oldOwner, _newOwner); 114: }
Use a 2 structure which is safer.
init()
function can be called anybody when the contract is not initialized.
More importantly, if someone else runs this function, they will have full authority because of the owner
state variable.
Here is a definition of init()
function.
contracts/smart-contract-wallet/SmartAccount.sol: 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: }
Add a control that makes init()
only call the Deployer Contract or EOA;
if (msg.sender != DEPLOYER_ADDRESS) { revert NotDeployer(); }
Any transaction has a 'base fee' of 21,000 gas in order to cover the cost of an elliptic curve operation that recovers the sender address from the signature, as well as the disk space of storing the transaction, according to the Ethereum White Paper
contracts/smart-contract-wallet/SmartAccount.sol: 192: function execTransaction( 193: Transaction memory _tx, 194: uint256 batchId, 195: FeeRefund memory refundInfo, 196: bytes memory signatures 197: ) public payable virtual override returns (bool success) { 198: // initial gas = 21k + non_zero_bytes * 16 + zero_bytes * 4 199: // ~= 21k + calldata.length * [1/3 * 16 + 2/3 * 4] 200: uint256 startGas = gasleft() + 21000 + msg.data.length * 8; 201: //console.log("init %s", 21000 + msg.data.length * 8); 202: bytes32 txHash; 203: // Use scope here to limit variable lifetime and prevent `stack too deep` errors 204: { 205: bytes memory txHashData = 206: encodeTransactionData( 207: // Transaction info 208: _tx, 209: // Payment info 210: refundInfo, 211: // Signature info 212: nonces[batchId] 213: ); 214: // Increase nonce and execute transaction. 215: // Default space aka batchId is 0 216: nonces[batchId]++; 217: txHash = keccak256(txHashData); 218: checkSignatures(txHash, txHashData, signatures); 219: }
https://ethereum-magicians.org/t/some-medium-term-dust-cleanup-ideas/6287
Why do txs cost 21000 gas? To understand how special-purpose cheap withdrawals could be done, it helps first to understand what goes into the 21000 gas in a tx. The cost of processing a tx includes: Two account writes (a balance-editing CALL normally costs 9000 gas) A signature verification (compare: the ECDSA precompile costs 3000 gas) The transaction data (~100 bytes, so 1600 gas, though originally it cost 6800) Some more gas was tacked on to account for transaction-specific overhead, bringing the total to 21000.
The minimum transaction value of 21,000 gas may change in the future, so it is recommended to make this value updatable.
Add this code;
uint256 txcost = 21_000; function changeTxCost(uint256 _amount) public onlyOwner { txcost = _amount; }
Description: The test coverage rate of the project is 76%. Testing all functions is best practice in terms of security criteria.
-------------------------------------------------------|----------|----------|----------|----------|----------------| File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines | -------------------------------------------------------|----------|----------|----------|----------|----------------| smart-contract-wallet/ | 35.1 | 16 | 29.87 | 35.79 | | BaseSmartAccount.sol | 0 | 0 | 0 | 0 |... 107,108,109 | Proxy.sol | 100 | 100 | 100 | 100 | | SmartAccount.sol | 63.92 | 32.2 | 52.94 | 64.23 |... 528,537,546 | SmartAccountFactory.sol | 81.82 | 50 | 75 | 76.47 | 54,56,59,60 | smart-contract-wallet/aa-4337/core/ | 98.46 | 81.15 | 93.18 | 97.16 | | EntryPoint.sol | 100 | 88.16 | 100 | 98.09 |373,421,463,471 | SenderCreator.sol | 100 | 100 | 100 | 100 | | StakeManager.sol | 100 | 76.92 | 100 | 100 | | smart-contract-wallet/aa-4337/interfaces/ | 80 | 25 | 80 | 84.62 | | UserOperation.sol | 80 | 25 | 80 | 84.62 | 53,78 | smart-contract-wallet/base/ | 24 | 17.86 | 27.27 | 23.08 | | Executor.sol | 75 | 75 | 100 | 100 | | FallbackManager.sol | 25 | 0 | 33.33 | 33.33 | 27,28,33,35 | ModuleManager.sol | 11.76 | 9.09 | 14.29 | 10.34 |... 124,126,129 | smart-contract-wallet/common/ | 25 | 0 | 33.33 | 33.33 | | Enum.sol | 100 | 100 | 100 | 100 | | SecuredTokenTransfer.sol | 100 | 100 | 100 | 100 | | SignatureDecoder.sol | 100 | 100 | 100 | 100 | | Singleton.sol | 0 | 100 | 0 | 0 | 13,15,22 | smart-contract-wallet/interfaces/ | 100 | 100 | 100 | 100 | | ERC1155TokenReceiver.sol | 100 | 100 | 100 | 100 | | ERC721TokenReceiver.sol | 100 | 100 | 100 | 100 | | ERC777TokensRecipient.sol | 100 | 100 | 100 | 100 | | IERC1271Wallet.sol | 100 | 100 | 100 | 100 | | IERC165.sol | 100 | 100 | 100 | 100 | | ISignatureValidator.sol | 100 | 100 | 100 | 100 | | smart-contract-wallet/libs/ | 1.45 | 1.47 | 13.64 | 2.7 | | LibAddress.sol | 0 | 100 | 0 | 0 | 12,14,15 | Math.sol | 0 | 0 | 0 | 0 |... 340,341,342 | MultiSend.sol | 100 | 50 | 100 | 100 | | MultiSendCallOnly.sol | 100 | 100 | 100 | 100 | | smart-contract-wallet/paymasters/ | 23.53 | 8.33 | 21.43 | 26.32 | | BasePaymaster.sol | 9.09 | 8.33 | 18.18 | 15.38 |... ,91,100,105 | PaymasterHelpers.sol | 50 | 100 | 33.33 | 50 | 28,44,45 | smart-contract-wallet/paymasters/verifying/singleton/ | 45 | 27.27 | 37.5 | 40.74 | | VerifyingSingletonPaymaster.sol | 45 | 27.27 | 37.5 | 40.74 |... 126,127,128 | -------------------------------------------------------|----------|----------|----------|----------|----------------|
Due to its capacity, test coverage is expected to be 100%.
Warning: Unused function parameter. Remove or comment out the variable name to silence this warning. --> contracts/smart-contract-wallet/paymasters/PaymasterHelpers.sol:25:9: | 25 | UserOperation calldata op, Warning: Unused local variable. --> contracts/smart-contract-wallet/utils/GasEstimatorSmartAccount.sol:20:5: | 20 | address _wallet = SmartAccountFactory(_factory).deployCounterFactualWallet(_owner, _entryPoint, _handler, _index);
Context:
3 results - 3 files contracts/smart-contract-wallet/SmartAccount.sol: 109: function setOwner(address _newOwner) external mixedAuth { contracts/smart-contract-wallet/base/ModuleManager.sol: 20: function setupModules(address to, bytes memory data) internal { contracts/smart-contract-wallet/paymasters/verifying/singleton/VerifyingSingletonPaymaster.sol: 65: function setSigner( address _newVerifyingSigner) external onlyOwner{
Checking whether the current value and the new value are the same should be added
Context: All Contracts
Description: It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI). It is clearly stated in the Solidity official documentation. In complex projects such as Defi, the interpretation of all functions and their arguments and returns is important for code readability and auditability. https://docs.soliditylang.org/en/v0.8.15/natspec-format.html
Recommendation: NatSpec comments should be increased in contracts
Function writing
that does not comply with the Solidity Style Guide
Context: All Contracts
Description: Order of Functions; ordering helps readers identify which functions they can call and to find the constructor and fallback definitions easier. But there are contracts in the project that do not comply with this.
https://docs.soliditylang.org/en/v0.8.17/style-guide.html
Functions should be grouped according to their visibility and ordered:
constructor receive function (if exists) fallback function (if exists) external public internal private within a grouping, place the view and pure functions last
It is a good practice to give time for users to react and adjust to critical changes. A timelock provides more guarantees and reduces the level of trust required, thus decreasing risk for users. It also indicates that the project is legitimate (less risk of a malicious owner making a sandwich attack on a user). Consider adding a timelock to:
contracts/smart-contract-wallet/SmartAccount.sol: 108 109: function setOwner(address _newOwner) external mixedAuth { 110: require(_newOwner != address(0), "Smart Account:: new Signatory address cannot be zero"); 111: address oldOwner = owner; 112: owner = _newOwner; 113: emit EOAChanged(address(this), oldOwner, _newOwner); 114: } 115
Context: All Contracts (116 results - 40 files)
Description:
Solidity code is also cleaner in another way that might not be noticeable: the struct Point. We were importing it previously with global import but not using it. The Point struct polluted the source code
with an unnecessary object we were not using because we did not need it.
This was breaking the rule of modularity and modular programming: only import what you need
Specific imports with curly braces allow us to apply this rule better.
Recommendation:
import {contract1 , contract2} from "filename.sol";
A good example from the ArtGobblers project;
import {Owned} from "solmate/auth/Owned.sol"; import {ERC721} from "solmate/tokens/ERC721.sol"; import {LibString} from "solmate/utils/LibString.sol"; import {MerkleProofLib} from "solmate/utils/MerkleProofLib.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {ERC1155, ERC1155TokenReceiver} from "solmate/tokens/ERC1155.sol"; import {toWadUnsafe, toDaysWadUnsafe} from "solmate/utils/SignedWadMath.sol";
Context: All Contracts
Description: It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI). It is clearly stated in the Solidity official documentation. In complex projects such as Defi, the interpretation of all functions and their arguments and returns is important for code readability and auditability.
https://docs.soliditylang.org/en/v0.8.15/natspec-format.html
Recommendation: Include return parameters in NatSpec comments
Recommendation Code Style: (from Uniswap3)
/// @notice Adds liquidity for the given recipient/tickLower/tickUpper position /// @dev The caller of this method receives a callback in the form of IUniswapV3MintCallback#uniswapV3MintCallback /// in which they must pay any token0 or token1 owed for the liquidity. The amount of token0/token1 due depends /// on tickLower, tickUpper, the amount of liquidity, and the current price. /// @param recipient The address for which the liquidity will be created /// @param tickLower The lower tick of the position in which to add liquidity /// @param tickUpper The upper tick of the position in which to add liquidity /// @param amount The amount of liquidity to mint /// @param data Any data that should be passed through to the callback /// @return amount0 The amount of token0 that was paid to mint the given amount of liquidity. Matches the value in the callback /// @return amount1 The amount of token1 that was paid to mint the given amount of liquidity. Matches the value in the callback function mint( address recipient, int24 tickLower, int24 tickUpper, uint128 amount, bytes calldata data ) external returns (uint256 amount0, uint256 amount1);
Context: EntryPoint.sol#L168 EntryPoint.sol#L289 EntryPoint.sol#L319 EntryPoint.sol#L349 EntryPoint.sol#L363 EntryPoint.sol#L409 EntryPoint.sol#L440 SmartAccount.sol#L239 SmartAccount.sol#L489
Description: It is generally recommended that lines in the source code should not exceed 80-120 characters. Today's screens are much larger, so in some cases it makes sense to expand that. The lines above should be split when they reach that length, as the files will most likely be on GitHub and GitHub always uses a scrollbar when the length is more than 164 characters.
(why-is-80-characters-the-standard-limit-for-code-width)[https://softwareengineering.stackexchange.com/questions/148677/why-is-80-characters-the-standard-limit-for-code-width]
Recommendation: Multiline output parameters and return statements should follow the same style recommended for wrapping long lines found in the Maximum Line Length section.
https://docs.soliditylang.org/en/v0.8.17/style-guide.html#introduction
thisFunctionCallIsReallyLong( longArgument1, longArgument2, longArgument3 );
Context:
23 results - 5 files contracts/smart-contract-wallet/SmartAccount.sol: 470: unchecked { contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol: 73: unchecked { 85: } //unchecked 185: unchecked { 249: unchecked { 291: unchecked { 350: unchecked { 417: unchecked { 442: unchecked { 477: } // unchecked 485: unchecked { contracts/smart-contract-wallet/aa-4337/interfaces/UserOperation.sol: 46: unchecked { contracts/smart-contract-wallet/libs/Math.sol: 60: unchecked { 179: unchecked { 195: unchecked { 207: unchecked { 248: unchecked { 260: unchecked { 297: unchecked { 311: unchecked { 340: unchecked { contracts/smart-contract-wallet/libs/Strings.sol: 19: unchecked { 44: unchecked {
Description: In total 5 contracts, 23 unchecked are used, the functions used are critical. For this reason, there must be fuzzing tests in the tests of the project. Not seen in tests.
Recommendation: Use should fuzzing test like Echidna.
As Alberto Cuesta Canada said: Fuzzing is not easy, the tools are rough, and the math is hard, but it is worth it. Fuzzing gives me a level of confidence in my smart contracts that I didn’t have before. Relying just on unit testing anymore and poking around in a testnet seems reckless now.
https://medium.com/coinmonks/smart-contract-fuzzing-d9b88e0b0a05
1 result - 1 file contracts/smart-contract-wallet/paymasters/verifying/singleton/VerifyingSingletonPaymaster.sol: 10: // import "../samples/Signatures.sol";
5 results - 3 files contracts/smart-contract-wallet/SmartAccount.sol: 445: return abi.encodePacked(bytes1(0x19), bytes1(0x01), domainSeparator(), safeTxHash); contracts/smart-contract-wallet/SmartAccountFactory.sol: 35: bytes memory deploymentData = abi.encodePacked(type(Proxy).creationCode, uint(uint160(_defaultImpl))); 54: bytes memory deploymentData = abi.encodePacked(type(Proxy).creationCode, uint(uint160(_defaultImpl))); 69: bytes memory code = abi.encodePacked(type(Proxy).creationCode, uint(uint160(_defaultImpl))); contracts/smart-contract-wallet/SmartAccountNoAuth.sol: 435: return abi.encodePacked(bytes1(0x19), bytes1(0x01), domainSeparator(), safeTxHash);
Rather than using abi.encodePacked
for appending bytes, since version 0.8.4, bytes.concat() is enabled
Since version 0.8.4 for appending bytes, bytes.concat() can be used instead of abi.encodePacked(,)
Context:
38 results - 11 files contracts/smart-contract-wallet/SmartAccount.sol: 181: function max(uint256 a, uint256 b) internal pure returns (uint256) { contracts/smart-contract-wallet/aa-4337/core/StakeManager.sol: 23: function getStakeInfo(address addr) internal view returns (StakeInfo memory info) { 38: function internalIncrementDeposit(address account, uint256 amount) internal { contracts/smart-contract-wallet/aa-4337/interfaces/UserOperation.sol: 36: function getSender(UserOperation calldata userOp) internal pure returns (address) { 45: function gasPrice(UserOperation calldata userOp) internal view returns (uint256) { 57: function pack(UserOperation calldata userOp) internal pure returns (bytes memory ret) { 73: function hash(UserOperation calldata userOp) internal pure returns (bytes32) { 77: function min(uint256 a, uint256 b) internal pure returns (uint256) { contracts/smart-contract-wallet/aa-4337/utils/Exec.sol: 13: ) internal returns (bool success) { 23: ) internal view returns (bool success) { 33: ) internal returns (bool success) { 40: function getReturnData() internal pure returns (bytes memory returnData) { 51: function revertWithData(bytes memory returnData) internal pure { 57: function callAndRevert(address to, bytes memory data) internal { contracts/smart-contract-wallet/base/Executor.sol: 19: ) internal returns (bool success) { contracts/smart-contract-wallet/base/FallbackManager.sol: 14: function internalSetFallbackHandler(address handler) internal { contracts/smart-contract-wallet/base/ModuleManager.sol: 16: address internal constant SENTINEL_MODULES = address(0x1); 18: mapping(address => address) internal modules; 20: function setupModules(address to, bytes memory data) internal { contracts/smart-contract-wallet/common/SecuredTokenTransfer.sol: 14: ) internal returns (bool transferred) { contracts/smart-contract-wallet/libs/LibAddress.sol: 11: function isContract(address account) internal view returns (bool) { contracts/smart-contract-wallet/libs/Math.sol: 19: function max(uint256 a, uint256 b) internal pure returns (uint256) { 26: function min(uint256 a, uint256 b) internal pure returns (uint256) { 34: function average(uint256 a, uint256 b) internal pure returns (uint256) { 45: function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) { 59: ) internal pure returns (uint256 result) { 145: ) internal pure returns (uint256) { 158: function sqrt(uint256 a) internal pure returns (uint256) { 194: function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) { 205: function log2(uint256 value) internal pure returns (uint256) { 247: function log2(uint256 value, Rounding rounding) internal pure returns (uint256) { 258: function log10(uint256 value) internal pure returns (uint256) { 296: function log10(uint256 value, Rounding rounding) internal pure returns (uint256) { 309: function log256(uint256 value) internal pure returns (uint256) { 339: function log256(uint256 value, Rounding rounding) internal pure returns (uint256) { contracts/smart-contract-wallet/paymasters/PaymasterHelpers.sol: 27: ) internal pure returns (bytes memory context) { 34: function decodePaymasterData(UserOperation calldata op) internal pure returns (PaymasterData memory) { 43: function decodePaymasterContext(bytes memory context) internal pure returns (PaymasterContext memory) {
Description: The above codes don't follow Solidity's standard naming convention,
internal and private functions : the mixedCase format starting with an underscore (_mixedCase starting with an underscore)
Throughout the codebase, events are generally emitted when sensitive changes are made to the contracts. However, some events are missing important parameters
The events should include the new value and old value where possible:
contracts/smart-contract-wallet/paymasters/BasePaymaster.sol: 23: 24: function setEntryPoint(IEntryPoint _entryPoint) public onlyOwner { 25: entryPoint = _entryPoint; 26: }
Context:
contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol: 255: // TODO: copy logic of gasPrice?
Recommendation: Use temporary TODOs as you work on a feature, but make sure to treat them before merging. Either add a link to a proper issue in your TODO, or remove it from the code.
external
contracts/smart-contract-wallet/SmartAccount.sol: 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: }
Description:
External instead of public would give more the sense of the init(...) functions to behave like a constructor (only called on deployment, so should only be called externally)
Security point of view, it might be safer so that it cannot be called internally by accident in the child contract
It might cost a bit less gas to use external over public
It is possible to override a function from external to public (= "opening it up") ✅ but it is not possible to override a function from public to external (= "narrow it down"). ❌
For above reasons you can change init(...) to external
https://github.com/OpenZeppelin/openzeppelin-contracts/issues/3750
contracts/smart-contract-wallet/SmartAccount.sol: - 200: uint256 startGas = gasleft() + 21000 + msg.data.length * 8; + uint256 startGas = gasleft() + 21_000 + msg.data.length * 8; contracts/smart-contract-wallet/common/SecuredTokenTransfer.sol: - 22: let success := call(sub(gas(), 10000), token, 0, add(data, 0x20), mload(data), 0, 0x20) + let success := call(sub(gas(), 10_000), token, 0, add(data, 0x20), mload(data), 0, 0x20)
Description: There is occasions where certain number have been hardcoded, either in variable or in the code itself. Large numbers can become hard to read.
Recommendation: Consider using underscores for number literals to improve its readability.
Empty blocks
should be removed or Emit somethingDescription: Code contains empty block
2 results - 2 files contracts/smart-contract-wallet/SmartAccount.sol: 550: receive() external payable {} contracts/smart-contract-wallet/aa-4337/samples/SimpleAccount.sol: 41: receive() external payable {}
Recommendation: The code should be refactored such that they no longer exist, or the block should do something useful, such as emitting an event or reverting.
require
instead of assert
2 results - 2 files contracts/smart-contract-wallet/Proxy.sol: 15 constructor(address _implementation) { 16: assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("biconomy.scw.proxy.implementation")) - 1)); contracts/smart-contract-wallet/common/Singleton.sol: 12 function _setImplementation(address _imp) internal { 13: assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("biconomy.scw.proxy.implementation")) - 1));
Description:
Assert should not be used except for tests, require
should be used
Prior to Solidity 0.8.0, pressing a confirm consumes the remainder of the process's available gas instead of returning it, as request()/revert() did.
assert() and ruqire();
The big difference between the two is that the assert()
function when false, uses up all the remaining gas and reverts all the changes made.
Meanwhile, a require()
function when false, also reverts back all the changes made to the contract but does refund all the remaining gas fees we offered to pay.
This is the most common Solidity function used by developers for debugging and error handling.
Assertion() should be avoided even after solidity version 0.8.0, because its documentation states "The Assert function generates an error of type Panic(uint256).Code that works properly should never Panic, even on invalid external input. If this happens, you need to fix it in your contract. there's a mistake".
As part of the upgradeability of Proxies , the contract can be upgraded multiple times, where it is a systematic approach to record the version of each upgrade.
contracts/smart-contract-wallet/SmartAccount.sol: 120: function updateImplementation(address _implementation) external mixedAuth { 121: require(_implementation.isContract(), "INVALID_IMPLEMENTATION"); 122: _setImplementation(_implementation); 123: // EOA + Version tracking 124: emit ImplementationUpdated(address(this), VERSION, _implementation); 125: }
I suggest implementing some kind of version counter that will be incremented automatically when you upgrade the contract.
Recommendation:
uint256 public VERSION; contracts/smart-contract-wallet/SmartAccount.sol: 120: function updateImplementation(address _implementation) external mixedAuth { 121: require(_implementation.isContract(), "INVALID_IMPLEMENTATION"); 122: _setImplementation(_implementation); 123: // EOA + Version tracking 124: emit ImplementationUpdated(address(this), ++VERSION, _implementation); 125: }
It can't be recovered if the tokens accidentally arrive at the contract address, which has happened to many popular projects, so I recommend adding a recovery code to your critical contracts.
Add this code:
/** * @notice Sends ERC20 tokens trapped in contract to external address * @dev Onlyowner is allowed to make this function call * @param account is the receiving address * @param externalToken is the token being sent * @param amount is the quantity being sent * @return boolean value indicating whether the operation succeeded. * */ function rescueERC20(address account, address externalToken, uint256 amount) public onlyOwner returns (bool) { IERC20(externalToken).transfer(account, amount); return true; } }
There are many addresses and constants used in the system. It is recommended to put the most used ones in one file (for example constants.sol, use inheritance to access these values)
This will help with readability and easier maintenance for future changes. This also helps with any issues, as some of these hard-coded values are admin addresses.
constants.sol Use and import this file in contracts that require access to these values. This is just a suggestion, in some use cases this may result in higher gas usage in the distribution.
10 results - 7 files contracts/smart-contract-wallet/Proxy.sol: 11: bytes32 internal constant _IMPLEMENTATION_SLOT = 0x37722d148fb373b961a84120b6c8d209709b45377878a466db32bbc40d95af26; contracts/smart-contract-wallet/SmartAccount.sol: 25: ISignatureValidatorConstants, 36: string public constant VERSION = "1.0.2"; // using AA 0.3.0 42: bytes32 internal constant DOMAIN_SEPARATOR_TYPEHASH = 0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218; 48: bytes32 internal constant ACCOUNT_TX_TYPEHASH = 0xc2595443c361a1f264c73470b9410fd67ac953ebd1a3ae63a2f514f3f014cf07; contracts/smart-contract-wallet/SmartAccountFactory.sol: 11: string public constant VERSION = "1.0.2"; contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol: 28: address private constant SIMULATE_FIND_AGGREGATOR = address(1); contracts/smart-contract-wallet/base/FallbackManager.sol: 12: bytes32 internal constant FALLBACK_HANDLER_STORAGE_SLOT = 0x6c9a6c4a39284e37ed1cf53d337577d14212a4870fb976a4366c693b939918d5; contracts/smart-contract-wallet/base/ModuleManager.sol: 16: address internal constant SENTINEL_MODULES = address(0x1); contracts/smart-contract-wallet/common/Singleton.sol: 10: bytes32 internal constant _IMPLEMENTATION_SLOT = 0x37722d148fb373b961a84120b6c8d209709b45377878a466db32bbc40d95af26;
Since this is a low level language that is more difficult to parse by readers, include extensive documentation, comments on the rationale behind its use, clearly explaining what each assembly instruction does
This will make it easier for users to trust the code, for reviewers to validate the code, and for developers to build on or update the code.
Note that using Aseembly removes several important security features of Solidity, which can make the code more insecure and more error-prone.
72 results - 22 files contracts/smart-contract-wallet/BaseSmartAccount.sol: 5: /* solhint-disable no-inline-assembly */ contracts/smart-contract-wallet/Proxy.sol: 17: assembly { 24: // solhint-disable-next-line no-inline-assembly 25: assembly { contracts/smart-contract-wallet/SmartAccount.sol: 142: // solhint-disable-next-line no-inline-assembly 143: assembly { 329: // solhint-disable-next-line no-inline-assembly 330: assembly { 337: // solhint-disable-next-line no-inline-assembly 338: assembly { 480: assembly { contracts/smart-contract-wallet/SmartAccountFactory.sol: 36: // solhint-disable-next-line no-inline-assembly 37: assembly { 55: // solhint-disable-next-line no-inline-assembly 56: assembly { contracts/smart-contract-wallet/SmartAccountNoAuth.sol: 142: // solhint-disable-next-line no-inline-assembly 143: assembly { 324: // solhint-disable-next-line no-inline-assembly 325: assembly { 332: // solhint-disable-next-line no-inline-assembly 333: assembly { 470: assembly { contracts/smart-contract-wallet/aa-4337/core/BaseAccount.sol: 5: /* solhint-disable no-inline-assembly */ contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol: 9: /* solhint-disable no-inline-assembly */ 501: assembly {offset := data} 505: assembly {data := offset} 512: assembly {mstore(0, number())} contracts/smart-contract-wallet/aa-4337/core/SenderCreator.sol: 19: /* solhint-disable no-inline-assembly */ 20: assembly { contracts/smart-contract-wallet/aa-4337/interfaces/IEntryPoint.sol: 9: /* solhint-disable no-inline-assembly */ contracts/smart-contract-wallet/aa-4337/interfaces/UserOperation.sol: 4: /* solhint-disable no-inline-assembly */ 39: assembly {data := calldataload(userOp)} 63: assembly { contracts/smart-contract-wallet/aa-4337/utils/Exec.sol: 4: // solhint-disable no-inline-assembly 14: assembly { 24: assembly { 34: assembly { 41: assembly { 52: assembly { contracts/smart-contract-wallet/base/Executor.sol: 21: // solhint-disable-next-line no-inline-assembly 22: assembly { 26: // solhint-disable-next-line no-inline-assembly 27: assembly { contracts/smart-contract-wallet/base/FallbackManager.sol: 16: // solhint-disable-next-line no-inline-assembly 17: assembly { 34: // solhint-disable-next-line no-inline-assembly 35: assembly { contracts/smart-contract-wallet/base/ModuleManager.sol: 87: // solhint-disable-next-line no-inline-assembly 88: assembly { 128: // solhint-disable-next-line no-inline-assembly 129: assembly { contracts/smart-contract-wallet/common/SecuredTokenTransfer.sol: 18: // solhint-disable-next-line no-inline-assembly 19: assembly { contracts/smart-contract-wallet/common/SignatureDecoder.sol: 22: // solhint-disable-next-line no-inline-assembly 23: assembly { contracts/smart-contract-wallet/common/Singleton.sol: 14: // solhint-disable-next-line no-inline-assembly 15: assembly { 21: // solhint-disable-next-line no-inline-assembly 22: assembly { contracts/smart-contract-wallet/libs/LibAddress.sol: 13: // solhint-disable-next-line no-inline-assembly 14: assembly { csize := extcodesize(account) } contracts/smart-contract-wallet/libs/Math.sol: 66: assembly { 86: assembly { 100: assembly { contracts/smart-contract-wallet/libs/MultiSend.sol: 28: // solhint-disable-next-line no-inline-assembly 29: assembly { contracts/smart-contract-wallet/libs/MultiSendCallOnly.sol: 22: // solhint-disable-next-line no-inline-assembly 23: assembly { contracts/smart-contract-wallet/libs/Strings.sol: 23: /// @solidity memory-safe-assembly 24: assembly { 29: /// @solidity memory-safe-assembly 30: assembly {
At the start of the project, the system may need to be stopped or upgraded, I suggest you have a script beforehand and add it to the documentation. This can also be called an " EMERGENCY STOP (CIRCUIT BREAKER) PATTERN ".
https://github.com/maxwoe/solidity_patterns/blob/master/security/EmergencyStop.sol
This codebase will be difficult to navigate, as there are no descriptive naming conventions that specify which files should contain meaningful logic.
Prefixes should be added like this by filing:
Interface I_ absctract contracts Abs_ Libraries Lib_
We recommend that you implement this or a similar agreement.
#0 - c4-judge
2023-01-22T15:28:57Z
gzeon-c4 marked the issue as grade-a
#1 - c4-judge
2023-01-22T15:59:19Z
gzeon-c4 marked the issue as selected for report
#2 - c4-sponsor
2023-02-09T12:39:02Z
livingrockrises marked the issue as sponsor confirmed
#3 - gzeon-c4
2023-02-20T05:03:42Z
lgtm
🌟 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
694.7104 USDC - $694.71
Number | Optimization Details | Context |
---|---|---|
[G-01] | With assembly, .call (bool success) transfer can be done gas-optimized | 8 |
[G-02] | Remove the initializer modifier | 1 |
[G-03] | Structs can be packed into fewer storage slots | 2 |
[G-04] | DepositInfo and PaymasterData structs can be rearranged | 2 |
[G-05] | Duplicated require()/if() checks should be refactored to a modifier or function | 5 |
[G-06] | Can be removed to 'assert' in function '_setImplementation' | 1 |
[G-07] | Instead of emit ExecutionSuccess and emit ExecutionFailure a single emit Execution is gas efficient | 1 |
[G-08] | Unnecessary computation | 1 |
[G-09] | Using delete instead of setting info struct to 0 saves gas | 3 |
[G-10] | Empty blocks should be removed or emit something | 1 |
[G-11] | Using storage instead of memory for structs/arrays saves gas | 11 |
[G-12] | Use Shift Right/Left instead of Division/Multiplication | 3 |
[G-13] | Use constants instead of type(uintx).max | 3 |
[G-14] | Add unchecked {} for subtractions where the operands cannot underflow because of a previous require or if statement | 2 |
[G-15] | Usage of uints/ints smaller than 32 bytes (256 bits) incurs overhead | 8 |
[G-16] | Reduce the size of error messages (Long revert Strings) | 13 |
[G-17] | Use double require instead of using && | 3 |
[G-18] | Use nested if and, avoid multiple check combinations | 5 |
[G-19] | Functions guaranteed to revert_ when callled by normal users can be marked payable | 18 |
[G-20] | Setting the constructor to payable | 4 |
[G-21] | Use assembly to write address storage values | 8 |
[G-22] | ++i/i++ should be unchecked{++i}/unchecked{i++} when it is not possible for them to overflow, as is the case when used in for- and while-loops | 6 |
[G-23] | Sort Solidity operations using short-circuit mode | 8 |
[G-24] | x += y (x -= y) costs more gas than x = x + y (x = x - y) for state variables | 3 |
[G-25] | Use a more recent version of solidity | 36 |
[G-26] | Optimize names to save gas | |
[G-27] | Upgrade Solidity's optimizer |
Total 27 issues
.call (bool success)
transfer can be done gas-optimizedreturn
data (bool success,)
has to be stored due to EVM architecture, but in a usage like below, 'out' and 'outsize' values are given (0,0), this storage disappears and gas optimization is provided.
https://twitter.com/pashovkrum/status/1607024043718316032?t=xs30iD6ORWtE2bTTYsCFIQ&s=19
There are 8 instances of the topic.
contracts\smart-contract-wallet\SmartAccount.sol#l451 449 function transfer(address payable dest, uint amount) external nonReentrant onlyOwner { - 451: (bool success,) = dest.call{value:amount}(""); + bool success; + assembly { + success := call(gas(), dest, amount, 0, 0) + } + 452 require(success,"transfer failed"); 453 } 454
contracts\smart-contract-wallet\SmartAccount.sol: 247 function handlePayment( 261: (bool success,) = receiver.call{value: payment}(""); 262 require(success, "BSA011");
271 function handlePaymentRevert( 285: (bool success,) = receiver.call{value: payment}(""); 286 require(success, "BSA011");
contracts\smart-contract-wallet\SmartAccount.sol: 525 function addDeposit() public payable { 526 527: (bool req,) = address(entryPoint()).call{value : msg.value}(""); 528 require(req); 529 }
contracts\smart-contract-wallet\aa-4337\core\EntryPoint.sol: 35 function _compensate(address payable beneficiary, uint256 amount) internal { 36 require(beneficiary != address(0), "AA90 invalid beneficiary"); 37: (bool success,) = beneficiary.call{value : amount}(""); 38 require(success, "AA91 failed send to beneficiary"); 39 }
contracts\smart-contract-wallet\aa-4337\core\StakeManager.sol: 96 function withdrawStake(address payable withdrawAddress) external { 106: (bool success,) = withdrawAddress.call{value : stake}(""); 107 require(success, "failed to withdraw stake"); 108 }
contracts\smart-contract-wallet\aa-4337\core\StakeManager.sol: 115 function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external { 120: (bool success,) = withdrawAddress.call{value : withdrawAmount}(""); 121 require(success, "failed to withdraw"); 122 }
contracts/smart-contract-wallet/BaseSmartAccount.sol: 106 function _payPrefund(uint256 missingAccountFunds) internal virtual { 108: (bool success,) = payable(msg.sender).call{value : missingAccountFunds, gas : type(uint256).max}(""); 109 (success);
initializer
modifierif we can just ensure that the initialize()
function could only be called from within the constructor, we shouldn't need to worry about it getting called again.
contracts/smart-contract-wallet/SmartAccount.sol: 166: function init(address _owner, address _entryPointAddress, address _handler) public override initializer {
In the EVM, the constructor's job is actually to return the bytecode that will live at the contract's address. So, while inside a constructor, your address (address(this))
will be the deployment address, but there will be no bytecode at that address! So if we check address(this).code.length
before the constructor has finished, even from within a delegatecall, we will get 0. So now let's update our initialize()
function to only run if we are inside a constructor:
contracts/smart-contract-wallet/SmartAccount.sol: 166: function init(address _owner, address _entryPointAddress, address _handler) public override initializer { + require(address(this).code.length == 0, 'not in constructor');
Now the Proxy contract's constructor can still delegatecall initialize(), but if anyone attempts to call it again (after deployment) through the Proxy instance, or tries to call it directly on the above instance, it will revert because address(this).code.length will be nonzero.
Also, because we no longer need to write to any state to track whether initialize() has been called, we can avoid the 20k storage gas cost. In fact, the cost for checking our own code size is only 2 gas, which means we have a 10,000x gas savings over the standard version. Pretty neat!
The UserOperation
struct can be packed into one slot less slot as suggested below.
scw-contracts\contracts\smart-contract-wallet\aa-4337\interfaces\UserOperation.sol: 19: struct UserOperation { 20 21 address sender; // slot0 (20 bytes) - 22 uint256 nonce; + 22 uint96 nonce; // slot0 (12 bytes) - 23 bytes initCode; - 24 bytes callData; - 25 uint256 callGasLimit; - 26 uint256 verificationGasLimit; - 27 uint256 preVerificationGas; 28 uint128 maxFeePerGas; // slot1 (16 bytes) 29 uint128 maxPriorityFeePerGas; // slot1 (16 bytes) + 25 uint256 callGasLimit; // slot2 (32 bytes) + 26 uint256 verificationGasLimit; // slot3 (32 bytes) + 27 uint256 preVerificationGas; // slot4 (32 bytes) + 23 bytes initCode; // slot5 (32 bytes) + 24 bytes callData; // slot6 (32 bytes) 30 bytes paymasterAndData; // slot7 (32 bytes) 31 bytes signature; // slot8 (32 bytes) 32 }
The MemoryUserOp
struct can be packed into one slot less slot as suggested below.
scw-contracts\contracts\smart-contract-wallet\aa-4337\core\EntryPoint.sol: 144 //a memory copy of UserOp fields (except that dynamic byte arrays: callData, initCode and signature 145: struct MemoryUserOp { 146 address sender; // slot0 - 147 uint256 nonce; + 147 uint96 nonce; // slot0 148 uint256 callGasLimit; // slot1 149 uint256 verificationGasLimit; // slot2 150 uint256 preVerificationGas; // slot3 151 address paymaster; // slot4 - 152 uint256 maxFeePerGas; + 152 uint128 maxFeePerGas; // slot5 - 153 uint256 maxPriorityFeePerGas; + 153 uint128 maxPriorityFeePerGas; // slot5 154 }
DepositInfo
and PaymasterData
structs can be rearrangedGas saving can be achieved by updating the DepositInfo
struct as below.
scw-contracts\contracts\smart-contract-wallet\aa-4337\interfaces\IStakeManager.sol: 53: struct DepositInfo { + StakeInfo stakeInfo; 54 uint112 deposit; 55 bool staked; - 56 uint112 stakes; - 57 uint32 unstakeDelaySec; 58 uint64 withdrawTime; 59 }
62: struct StakeInfo { 63 uint256 stakes; 64 uint256 unstakeDelaySec; 65 }
Gas saving can be achieved by updating the PaymasterData
struct as below.
scw-contracts\contracts\smart-contract-wallet\paymasters\PaymasterHelpers.sol: 7: struct PaymasterData { + PaymasterContext paymasterContex; - 8: address paymasterId; 9: bytes signature; 10: uint256 signatureLength; 11: }
13: struct PaymasterContext { 14: address paymasterId; 15: //@review 16: }
contracts\smart-contract-wallet\SmartAccount.sol: 258: if (gasToken == address(0)) { 282: if (gasToken == address(0)) {
SmartAccount.sol#L258, SmartAccount.sol#L282
contracts\smart-contract-wallet\aa-4337\core\EntryPoint.sol: 315: if (paymaster == address(0)) { 330: if (paymaster == address(0)) { 448: if (paymaster == address(0)) { 321: if (_deadline != 0 && _deadline < block.timestamp) { 365: if (_deadline != 0 && _deadline < block.timestamp) { 488: if (maxFeePerGas == maxPriorityFeePerGas) {
EntryPoint.sol#L315, EntryPoint.sol#L330, EntryPoint.sol#L448
EntryPoint.sol#L321, EntryPoint.sol#L365
contracts\smart-contract-wallet\aa-4337\interfaces\UserOperation.sol: 49: if (maxFeePerGas == maxPriorityFeePerGas) {
contracts\smart-contract-wallet\base\ModuleManager.sol: 34: require(module != address(0) && module != SENTINEL_MODULES, "BSA101"); 49: require(module != address(0) && module != SENTINEL_MODULES, "BSA101");
ModuleManager.sol#L34, ModuleManager.sol#L49
Recommendation:
You can consider adding a modifier like below.
modifer check (address checkToAddress) { require(checkToAddress != address(0) && checkToAddress != SENTINEL_MODULES, "BSA101"); _; }
Here are the data available in the covered contracts. The use of this situation in contracts that are not covered will also provide gas optimization.
The state variable _IMPLEMENTATION_SLOT constant is precomputed keccak256("biconomy.scw.proxy.implementation") - 1
. The assert check here is unnecessary. Removing this control provides gas optimization.
contracts\smart-contract-wallet\common\Singleton.sol: 12 function _setImplementation(address _imp) internal { - 13: assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("biconomy.scw.proxy.implementation")) - 1)); 14 // solhint-disable-next-line no-inline-assembly 15 assembly { 16 sstore(_IMPLEMENTATION_SLOT, _imp) 17 } 18 }
Proof of Concept: The optimizer was turned on and set to 200 runs test was done in 0.8.12
contract GasTest is DSTest { Contract0 c0; Contract1 c1; function setUp() public { c0 = new Contract0(); c1 = new Contract1(); } function testGas() public { c0._setImplementation(0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2); c1._setImplementation(0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2); } } contract Contract0 { // singleton slot always needs to be first declared variable, to ensure that it is at the same location as in the Proxy contract. /* This is the keccak-256 hash of "biconomy.scw.proxy.implementation" subtracted by 1 */ bytes32 internal constant _IMPLEMENTATION_SLOT = 0x37722d148fb373b961a84120b6c8d209709b45377878a466db32bbc40d95af26; function _setImplementation(address _imp) public { assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("biconomy.scw.proxy.implementation")) - 1)); // solhint-disable-next-line no-inline-assembly assembly { sstore(_IMPLEMENTATION_SLOT, _imp) } } } contract Contract1 { // singleton slot always needs to be first declared variable, to ensure that it is at the same location as in the Proxy contract. /* This is the keccak-256 hash of "biconomy.scw.proxy.implementation" subtracted by 1 */ bytes32 internal constant _IMPLEMENTATION_SLOT = 0x37722d148fb373b961a84120b6c8d209709b45377878a466db32bbc40d95af26; function _setImplementation(address _imp) public { assembly { sstore(_IMPLEMENTATION_SLOT, _imp) } } }
Gas Report:
╭──────────────────────────────────────┬─────────────────┬───────┬────────┬───────┬─────────╮ │ src/test/test.sol:Contract0 contract ┆ ┆ ┆ ┆ ┆ │ ╞══════════════════════════════════════╪═════════════════╪═══════╪════════╪═══════╪═════════╡ │ Deployment Cost ┆ Deployment Size ┆ ┆ ┆ ┆ │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤ │ 71123 ┆ 387 ┆ ┆ ┆ ┆ │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤ │ Function Name ┆ min ┆ avg ┆ median ┆ max ┆ # calls │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤ │ _setImplementation ┆ 22435 ┆ 22435 ┆ 22435 ┆ 22435 ┆ 1 │ ╰──────────────────────────────────────┴─────────────────┴───────┴────────┴───────┴─────────╯ ╭──────────────────────────────────────┬─────────────────┬───────┬────────┬───────┬─────────╮ │ src/test/test.sol:Contract1 contract ┆ ┆ ┆ ┆ ┆ │ ╞══════════════════════════════════════╪═════════════════╪═══════╪════════╪═══════╪═════════╡ │ Deployment Cost ┆ Deployment Size ┆ ┆ ┆ ┆ │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤ │ 38893 ┆ 225 ┆ ┆ ┆ ┆ │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤ │ Function Name ┆ min ┆ avg ┆ median ┆ max ┆ # calls │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤ │ _setImplementation ┆ 22336 ┆ 22336 ┆ 22336 ┆ 22336 ┆ 1 │ ╰──────────────────────────────────────┴─────────────────┴───────┴────────┴───────┴─────────╯
emit ExecutionSuccess
and emit ExecutionFailure
a single emit Execution
is gas efficientIf the emit ExecutionSuccess
and emit ExecutionFailure
at the end of the execute
function are removed and arranged as follows, gas savings will be achieved. The last element of the event Execution
bool success will indicate whether the operation was successful or unsuccessful, with a value of true or false.
contracts\smart-contract-wallet\base\Executor.sol: - event ExecutionFailure(address to, uint256 value, bytes data, Enum.Operation operation, uint256 txGas); - event ExecutionSuccess(address to, uint256 value, bytes data, Enum.Operation operation, uint256 txGas); + event Execution(address to, uint256 value, bytes data, Enum.Operation operation, uint256 txGas, bool success); 13 function execute( 31 // Emit events here.. - 32: if (success) emit ExecutionSuccess(to, value, data, operation, txGas); - 33: else emit ExecutionFailure(to, value, data, operation, txGas); + emit Execution (to, value, data, operation, txGas, success); 34 } 35 36 }
When emitting an event that includes a new and an old value, it is cheaper in gas to avoid caching the old value in memory. Instead, emit the event, then save the new value in storage.
contracts\smart-contract-wallet\SmartAccount.sol: 111 address oldOwner = owner; + 113: emit EOAChanged(address(this), oldOwner, _newOwner); 112 owner = _newOwner; - 113: emit EOAChanged(address(this), oldOwner, _newOwner); 114 }
info
struct to 0 saves gascontracts\smart-contract-wallet\aa-4337\core\StakeManager.sol: 96 function withdrawStake(address payable withdrawAddress) external { 97 DepositInfo storage info = deposits[msg.sender]; 102: info.unstakeDelaySec = 0; 103: info.withdrawTime = 0; 104: info.stake = 0;
The code should be refactored such that they no longer exist, or the block should do something useful, such as emitting an event or reverting. If the contract is meant to be extended, the contract should be abstract and the function signatures be added without any default implementation. If the block is an empty if-statement block to avoid doing subsequent checks in the else-if/else conditions, the else-if/else conditions should be nested under the negation of the if-statement, because they involve different classes of checks, which may lead to the introduction of errors when the code is later modified (if(x){}else if(y){...}else{...} => if(!x){if(y){...}else{...}}). Empty receive()/fallback() payable functions that are not used, can be removed to save deployment gas.
contracts/smart-contract-wallet/SmartAccount.sol: 550: receive() external payable {}
storage
instead of memory
for structs/arrays
saves gasWhen fetching data from a storage location, assigning the data to a memory
variable causes 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 declearing 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 incuring 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
11 results - 2 files:
contracts\smart-contract-wallet\aa-4337\core\EntryPoint.sol: 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); 238: StakeInfo memory factoryInfo = getStakeInfo(factory); 241: AggregatorStakeInfo memory aggregatorInfo = AggregatorStakeInfo(aggregator, getStakeInfo(aggregator)); 293: MemoryUserOp memory mUserOp = opInfo.mUserOp; 351: MemoryUserOp memory mUserOp = opInfo.mUserOp; 389: MemoryUserOp memory mUserOp = outOpInfo.mUserOp; 444: MemoryUserOp memory mUserOp = opInfo.mUserOp;
EntryPoint.sol#L171, EntryPoint.sol#L229, EntryPoint.sol#L234, EntryPoint.sol#L235, EntryPoint.sol#L238, EntryPoint.sol#L241, EntryPoint.sol#L293, EntryPoint.sol#L351, EntryPoint.sol#L389, EntryPoint.sol#L444
contracts\smart-contract-wallet\paymasters\verifying\singleton\VerifyingSingletonPaymaster.sol: 126: PaymasterContext memory data = context.decodePaymasterContext();
VerifyingSingletonPaymaster.sol#L126
A division/multiplication by any number x being a power of 2 can be calculated by shifting to the right/left. While the DIV opcode uses 5 gas, the SHR opcode only uses 3 gas.
Furthermore, Solidity's division operation also includes a division-by-0 prevention which is bypassed using shifting.
3 results 2 files:
contracts/smart-contract-wallet/libs/Math.sol: - 36: return (a & b) + (a ^ b) / 2; + 36: return (a & b) + (a ^ b) >> 1;
contracts/smart-contract-wallet/SmartAccount.sol: - 200: uint256 startGas = gasleft() + 21000 + msg.data.length * 8; + 200: uint256 startGas = gasleft() + 21000 + msg.data.length << 3;
- 224: require(gasleft() >= max((_tx.targetTxGas * 64) / 63,_tx.targetTxGas + 2500) + 500, "BSA010"); + 224: require(gasleft() >= max((_tx.targetTxGas << 6) / 63,_tx.targetTxGas + 2500) + 500, "BSA010");
type(uint120).max or type(uint112).max, etc. it uses more gas in the distribution process and also for each transaction than constant usage.
3 results - 2 files:
contracts\smart-contract-wallet\aa-4337\core\EntryPoint.sol: 397: require(maxGasValues <= type(uint120).max, "AA94 gas values overflow");
contracts\smart-contract-wallet\aa-4337\core\StakeManager.sol: 41: require(newAmount <= type(uint112).max, "deposit overflow"); 65: require(stake < type(uint112).max, "stake overflow");
StakeManager.sol#L41, StakeManager.sol#L65
unchecked {}
for subtractions where the operands cannot underflow because of a previous require
or if
statementrequire(a <= b); x = b - a => require(a <= b); unchecked { x = b - a } if(a <= b); x = b - a => if(a <= b); unchecked { x = b - a }
This will stop the check for overflow and underflow so it will save gas.
2 results - 2 files:
contracts/smart-contract-wallet/aa-4337/core/StakeManager.sol: 116 DepositInfo storage info = deposits[msg.sender]; 117 require(withdrawAmount <= info.deposit, "Withdraw amount too large"); 118: info.deposit = uint112(info.deposit - withdrawAmount);
contracts/smart-contract-wallet/paymasters/verifying/singleton/VerifyingSingletonPaymaster.sol: 56 uint256 currentBalance = paymasterIdBalances[msg.sender]; 57 require(amount <= currentBalance, "Insufficient amount to withdraw"); 58: paymasterIdBalances[msg.sender] -= amount;
VerifyingSingletonPaymaster.sol#L58
When using elements that are smaller than 32 bytes, your contracts gas usage may be higher. This is because the EVM operates on 32 bytes at a time. Therefore, if the element is smaller than that, the EVM must use more operations in order to reduce the size of the element from 32 bytes to the desired size.
https://docs.soliditylang.org/en/v0.8.11/internals/layout_in_storage.html
Use a larger size then downcast where needed.
8 results - 3 files:
contracts\smart-contract-wallet\aa-4337\core\StakeManager.sol: 42: info.deposit = uint112(newAmount); 59: function addStake(uint32 _unstakeDelaySec) public payable { 69: uint112(stake), 84: uint64 withdrawTime = uint64(block.timestamp) + info.unstakeDelaySec; 118: info.deposit = uint112(info.deposit - withdrawAmount);
StakeManager.sol#L42, StakeManager.sol#L59, StakeManager.sol#L69, StakeManager.sol#L84, StakeManager.sol#L118
contracts\smart-contract-wallet\paymasters\BasePaymaster.sol: 75: function addStake(uint32 unstakeDelaySec) external payable onlyOwner {
contracts\smart-contract-wallet\aa-4337\core\EntryPoint.sol: 336: senderInfo.deposit = uint112(deposit - requiredPrefund); 362: paymasterInfo.deposit = uint112(deposit - requiredPreFund);
EntryPoint.sol#L336, EntryPoint.sol#L362
Shortening revert strings to fit in 32 bytes will decrease deployment time gas and will decrease runtime gas when the revert condition is met.
Revert strings that are longer than 32 bytes require at least one additional mstore, along with additional overhead for computing memory offset, etc.
13 results - 4 files:
contracts/smart-contract-wallet/SmartAccount.sol: 77: require(msg.sender == owner, "Smart Account:: Sender is not authorized"); 110: require(_newOwner != address(0), "Smart Account:: new Signatory address cannot be zero"); 128: require(_newEntryPoint != address(0), "Smart Account:: new entry point address cannot be zero");
SmartAccount.sol#L77, SmartAccount.sol#L110, SmartAccount.sol#L128
contracts/smart-contract-wallet/SmartAccountFactory.sol: 18: require(_baseImpl != address(0), "base wallet address can not be zero");
contracts/smart-contract-wallet/libs/MultiSend.sol: 27: require(address(this) != multisendSingleton, "MultiSend should only be called via delegatecall");
contracts/smart-contract-wallet/paymasters/verifying/singleton/VerifyingSingletonPaymaster.sol: 36: require(address(_entryPoint) != address(0), "VerifyingPaymaster: Entrypoint can not be zero address"); 37: require(_verifyingSigner != address(0), "VerifyingPaymaster: signer of paymaster can not be zero address"); 49: require(!Address.isContract(paymasterId), "Paymaster Id can not be smart contract address"); 50: require(paymasterId != address(0), "Paymaster Id can not be zero address"); 66: require(_newVerifyingSigner != address(0), "VerifyingPaymaster: new signer can not be zero address"); 107: require(sigLength == 64 || sigLength == 65, "VerifyingPaymaster: invalid signature length in paymasterAndData"); 108: require(verifyingSigner == hash.toEthSignedMessageHash().recover(paymasterData.signature), "VerifyingPaymaster: wrong signature"); 109: require(requiredPreFund <= paymasterIdBalances[paymasterData.paymasterId], "Insufficient balance for paymaster id");
VerifyingSingletonPaymaster.sol#L36, VerifyingSingletonPaymaster.sol#L37, VerifyingSingletonPaymaster.sol#L49, VerifyingSingletonPaymaster.sol#L50, VerifyingSingletonPaymaster.sol#L66, VerifyingSingletonPaymaster.sol#L107, VerifyingSingletonPaymaster.sol#L108, VerifyingSingletonPaymaster.sol#L109
Recommendation: Revert strings > 32 bytes or use Custom Error()
double require
instead of using &&
Using double require instead of operator && can save more gas When having a require statement with 2 or more expressions needed, place the expression that cost less gas first.
3 results - 1 file contracts\smart-contract-wallet\base\ModuleManager.sol: 34: require(module != address(0) && module != SENTINEL_MODULES, "BSA101"); 49: require(module != address(0) && module != SENTINEL_MODULES, "BSA101"); 68: require(msg.sender != SENTINEL_MODULES && modules[msg.sender] != address(0), "BSA104");
ModuleManager.sol#L34, ModuleManager.sol#L49, ModuleManager.sol#L68
Recommendation Code:
contracts\smart-contract-wallet\base\ModuleManager.sol#L68 - 68: require(msg.sender != SENTINEL_MODULES && modules[msg.sender] != address(0), "BSA104"); + require(msg.sender != SENTINEL_MODULES, "BSA104"); + require(modules[msg.sender] != address(0), "BSA104");
Using nested is cheaper than using && multiple check combinations. There are more advantages, such as easier to read code and better coverage reports.
5 results - 2 files:
contracts\smart-contract-wallet\aa-4337\core\EntryPoint.sol: 303: if (mUserOp.paymaster != address(0) && mUserOp.paymaster.code.length == 0) { 321: if (_deadline != 0 && _deadline < block.timestamp) { 365: if (_deadline != 0 && _deadline < block.timestamp) { 410: if (paymasterDeadline != 0 && paymasterDeadline < deadline) {
EntryPoint.sol#L303, EntryPoint.sol#L321, EntryPoint.sol#L365, EntryPoint.sol#L410
contracts\smart-contract-wallet\libs\Math.sol: 147: if (rounding == Rounding.Up && mulmod(x, y, denominator) > 0) {
Recomendation Code:
contracts\smart-contract-wallet\aa-4337\core\EntryPoint.sol#L410 - 410: if (paymasterDeadline != 0 && paymasterDeadline < deadline) { + if (paymasterDeadline != 0) { + if (paymasterDeadline < deadline) { + } + }
payable
If a function modifier or require such as onlyOwner-admin is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided. The extra opcodes avoided are CALLVALUE(2), DUP1(3), ISZERO(3), PUSH2(3), JUMPI(10), PUSH1(3), DUP1(3), REVERT(0), JUMPDEST(1), POP(2) which costs an average of about 21 gas per call to the function, in addition to the extra deployment cost.
18 results - 5 files:
contracts\smart-contract-wallet\SmartAccount.sol: 109: function setOwner(address _newOwner) external mixedAuth { 120: function updateImplementation(address _implementation) external mixedAuth { 127: function updateEntryPoint(address _newEntryPoint) external mixedAuth { 449: function transfer(address payable dest, uint amount) external nonReentrant onlyOwner { 455: function pullTokens(address token, address dest, uint256 amount) external onlyOwner { 460: function execute(address dest, uint value, bytes calldata func) external onlyOwner{ 465: function executeBatch(address[] calldata dest, bytes[] calldata func) external onlyOwner{ 489: function execFromEntryPoint(address dest, uint value, bytes calldata func, Enum.Operation operation, uint256 gasLimit) external onlyEntryPoint returns (bool success) { 536: function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public onlyOwner {
SmartAccount.sol#L109, SmartAccount.sol#L120, SmartAccount.sol#L127, SmartAccount.sol#L449, SmartAccount.sol#L455, SmartAccount.sol#L460, SmartAccount.sol#L465, SmartAccount.sol#L489, SmartAccount.sol#L536
contracts\smart-contract-wallet\paymasters\verifying\singleton\VerifyingSingletonPaymaster.sol: 65: function setSigner( address _newVerifyingSigner) external onlyOwner{
VerifyingSingletonPaymaster.sol#L65
contracts\smart-contract-wallet\base\FallbackManager.sol: 26: function setFallbackHandler(address handler) public authorized {
contracts\smart-contract-wallet\base\ModuleManager.sol: 32: function enableModule(address module) public authorized { 47: function disableModule(address prevModule, address module) public authorized {
ModuleManager.sol#L32, ModuleManager.sol#L47
contracts\smart-contract-wallet\paymasters\BasePaymaster.sol: 24: function setEntryPoint(IEntryPoint _entryPoint) public onlyOwner { 67: function withdrawTo(address payable withdrawAddress, uint256 amount) public virtual onlyOwner { 75: function addStake(uint32 unstakeDelaySec) external payable onlyOwner { 90: function unlockStake() external onlyOwner { 99: function withdrawStake(address payable withdrawAddress) external onlyOwner {
BasePaymaster.sol#L24, BasePaymaster.sol#L67, BasePaymaster.sol#L75, BasePaymaster.sol#L90, BasePaymaster.sol#L99
Recommendation:
Functions guaranteed to revert when called by normal users can be marked payable (for only onlyOwner, mixedAuth, authorized
functions)
payable
You can cut out 10 opcodes in the creation-time EVM bytecode if you declare a constructor payable. Making the constructor payable eliminates the need for an initial check of msg.value == 0
and saves 13 gas
on deployment with no security risks.
Context: 4 results - 4 files:
contracts/smart-contract-wallet/Proxy.sol: 15: constructor(address _implementation) {
contracts/smart-contract-wallet/SmartAccountFactory.sol: 17: constructor(address _baseImpl) {
contracts/smart-contract-wallet/libs/MultiSend.sol: 12: constructor() {
contracts/smart-contract-wallet/paymasters/verifying/singleton/VerifyingSingletonPaymaster.sol: 35: constructor(IEntryPoint _entryPoint, address _verifyingSigner) BasePaymaster(_entryPoint) {
VerifyingSingletonPaymaster.sol#L35
contracts\smart-contract-wallet\paymasters\BasePaymaster.sol: 20: constructor(IEntryPoint _entryPoint) {
Recommendation:
Set the constructor to payable
assembly
to write address storage values8 results - 4 files:
contracts/smart-contract-wallet/SmartAccountFactory.sol: 17 constructor(address _baseImpl) { 19: _defaultImpl = _baseImpl;
contracts/smart-contract-wallet/SmartAccount.sol: 109 function setOwner(address _newOwner) external mixedAuth { 112: owner = _newOwner; 127 function updateEntryPoint(address _newEntryPoint) external mixedAuth { 130: _entryPoint = IEntryPoint(payable(_newEntryPoint)); 166 function init(address _owner, address _entryPointAddress, address _handler) public override initializer { 172: owner = _owner; 173: _entryPoint = IEntryPoint(payable(_entryPointAddress));
SmartAccount.sol#L112, SmartAccount.sol#L130, SmartAccount.sol#L172-L173
contracts/smart-contract-wallet/paymasters/verifying/singleton/VerifyingSingletonPaymaster.sol: 35 constructor(IEntryPoint _entryPoint, address _verifyingSigner) BasePaymaster(_entryPoint) { 38: verifyingSigner = _verifyingSigner; 65 function setSigner( address _newVerifyingSigner) external onlyOwner{ 67: verifyingSigner = _newVerifyingSigner;
VerifyingSingletonPaymaster.sol#L38, VerifyingSingletonPaymaster.sol#L67
contracts/smart-contract-wallet/paymasters/BasePaymaster.sol: 24 function setEntryPoint(IEntryPoint _entryPoint) public onlyOwner { 25: entryPoint = _entryPoint; 26 }
Recommendation Code:
contracts/smart-contract-wallet/paymasters/BasePaymaster.sol#L25 function setEntryPoint(IEntryPoint _entryPoint) public onlyOwner { assembly { sstore(entryPoint.slot, _entryPoint) } }
The unchecked keyword is new in solidity version 0.8.0, so this only applies to that version or higher, which these instances are.
6 results - 2 files:
contracts\smart-contract-wallet\SmartAccount.sol: 468: for (uint i = 0; i < dest.length;) {
contracts\smart-contract-wallet\aa-4337\core\EntryPoint.sol: 100: for (uint256 i = 0; i < opasLen; i++) { 107: for (uint256 a = 0; a < opasLen; a++) { 112: for (uint256 i = 0; i < opslen; i++) { 128: for (uint256 a = 0; a < opasLen; a++) { 134: for (uint256 i = 0; i < opslen; i++) {
EntryPoint.sol#L100, EntryPoint.sol#L107, EntryPoint.sol#L112, EntryPoint.sol#L128, EntryPoint.sol#L134
Short-circuiting is a solidity contract development model that uses OR/AND
logic to sequence different cost operations. It puts low gas cost operations in the front and high gas cost operations in the back, so that if the front is low If the cost operation is feasible, you can skip (short-circuit) the subsequent high-cost Ethereum virtual machine operation.
//f(x) is a low gas cost operation //g(y) is a high gas cost operation //Sort operations with different gas costs as follows f(x) || g(y) f(x) && g(y)
8 results - 2 files:
contracts/smart-contract-wallet/SmartAccount.sol: 83: require(msg.sender == owner || msg.sender == address(this),"Only owner or self"); 232: require(success || _tx.targetTxGas != 0 || refundInfo.gasPrice != 0, "BSA013"); 495: require(msg.sender == address(entryPoint()) || msg.sender == owner, "account: not Owner or EntryPoint"); 511: require(owner == hash.recover(userOp.signature) || tx.origin == address(0), "account: wrong signature");
SmartAccount.sol#L83, SmartAccount.sol#L232, SmartAccount.sol#L495, SmartAccount.sol#L511
contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol: 303: if (mUserOp.paymaster != address(0) && mUserOp.paymaster.code.length == 0) { 321: if (_deadline != 0 && _deadline < block.timestamp) { 365: if (_deadline != 0 && _deadline < block.timestamp) { 410: if (paymasterDeadline != 0 && paymasterDeadline < deadline) {
EntryPoint.sol#L303, EntryPoint.sol#L321, EntryPoint.sol#L365, EntryPoint.sol#L410
x += y (x -= y)
costs more gas than x = x + y (x = x - y)
for state variables3 results - 1 file:
contracts\smart-contract-wallet\paymasters\verifying\singleton\VerifyingSingletonPaymaster.sol: 51: paymasterIdBalances[paymasterId] += msg.value; 58: paymasterIdBalances[msg.sender] -= amount; 128: paymasterIdBalances[extractedPaymasterId] -= actualGasCost;
VerifyingSingletonPaymaster.sol#L51, VerifyingSingletonPaymaster.sol#L58, VerifyingSingletonPaymaster.sol#L128
contracts\smart-contract-wallet\paymasters\verifying\singleton\VerifyingSingletonPaymaster.sol#L128 - 128: paymasterIdBalances[extractedPaymasterId] -= actualGasCost; + 128: paymasterIdBalances[extractedPaymasterId] = paymasterIdBalances[extractedPaymasterId] - actualGasCost;
x += y (x -= y)
costs more gas than x = x + y (x = x - y)
for state variables.
In 0.8.15 the conditions necessary for inlining are relaxed. Benchmarks show that the change significantly decreases the bytecode size (which impacts the deployment cost) while the effect on the runtime gas usage is smaller.
In 0.8.17 prevent the incorrect removal of storage writes before calls to Yul functions that conditionally terminate the external EVM call; Simplify the starting offset of zero-length operations to zero. More efficient overflow checks for multiplication.
36 results - 36 files: contracts/smart-contract-wallet/BaseSmartAccount.sol: 2: pragma solidity 0.8.12; contracts/smart-contract-wallet/Proxy.sol: 2: pragma solidity 0.8.12; contracts/smart-contract-wallet/SmartAccount.sol: 2: pragma solidity 0.8.12; contracts/smart-contract-wallet/SmartAccountFactory.sol: 2: pragma solidity 0.8.12; contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol: 6: pragma solidity ^0.8.12; contracts/smart-contract-wallet/aa-4337/core/SenderCreator.sol: 2: pragma solidity ^0.8.12; contracts/smart-contract-wallet/aa-4337/core/StakeManager.sol: 2: pragma solidity ^0.8.12; contracts/smart-contract-wallet/aa-4337/interfaces/IAccount.sol: 2: pragma solidity ^0.8.12; contracts/smart-contract-wallet/aa-4337/interfaces/IAggregatedAccount.sol: 2: pragma solidity ^0.8.12; contracts/smart-contract-wallet/aa-4337/interfaces/IAggregator.sol: 2: pragma solidity 0.8.12; contracts/smart-contract-wallet/aa-4337/interfaces/IEntryPoint.sol: 6: pragma solidity ^0.8.12; contracts/smart-contract-wallet/aa-4337/interfaces/IPaymaster.sol: 2: pragma solidity ^0.8.12; contracts/smart-contract-wallet/aa-4337/interfaces/IStakeManager.sol: 2: pragma solidity ^0.8.12; contracts/smart-contract-wallet/aa-4337/interfaces/UserOperation.sol: 2: pragma solidity ^0.8.12; contracts/smart-contract-wallet/aa-4337/utils/Exec.sol: 2: pragma solidity >=0.7.5 <0.9.0; contracts/smart-contract-wallet/base/Executor.sol: 2: pragma solidity 0.8.12; contracts/smart-contract-wallet/base/FallbackManager.sol: 2: pragma solidity 0.8.12; contracts/smart-contract-wallet/base/ModuleManager.sol: 2: pragma solidity 0.8.12; contracts/smart-contract-wallet/common/Enum.sol: 2: pragma solidity 0.8.12; contracts/smart-contract-wallet/common/SecuredTokenTransfer.sol: 2: pragma solidity 0.8.12; contracts/smart-contract-wallet/common/SignatureDecoder.sol: 2: pragma solidity 0.8.12; contracts/smart-contract-wallet/common/Singleton.sol: 2: pragma solidity 0.8.12; contracts/smart-contract-wallet/handler/DefaultCallbackHandler.sol: 2: pragma solidity 0.8.12; contracts/smart-contract-wallet/interfaces/ERC721TokenReceiver.sol: 2: pragma solidity 0.8.12; contracts/smart-contract-wallet/interfaces/ERC777TokensRecipient.sol: 2: pragma solidity 0.8.12; contracts/smart-contract-wallet/interfaces/ERC1155TokenReceiver.sol: 2: pragma solidity 0.8.12; contracts/smart-contract-wallet/interfaces/IERC165.sol: 2: pragma solidity 0.8.12; contracts/smart-contract-wallet/interfaces/IERC1271Wallet.sol: 2: pragma solidity 0.8.12; contracts/smart-contract-wallet/interfaces/ISignatureValidator.sol: 2: pragma solidity 0.8.12; contracts/smart-contract-wallet/libs/LibAddress.sol: 2: pragma solidity 0.8.12; contracts/smart-contract-wallet/libs/Math.sol: 4: pragma solidity 0.8.12; contracts/smart-contract-wallet/libs/MultiSend.sol: 2: pragma solidity 0.8.12; contracts/smart-contract-wallet/libs/MultiSendCallOnly.sol: 2: pragma solidity 0.8.12; contracts/smart-contract-wallet/paymasters/BasePaymaster.sol: 2: pragma solidity ^0.8.12; contracts/smart-contract-wallet/paymasters/PaymasterHelpers.sol: 2: pragma solidity 0.8.12; contracts/smart-contract-wallet/paymasters/verifying/singleton/VerifyingSingletonPaymaster.sol: 2: pragma solidity 0.8.12;
Contracts most called functions could simply save gas by function ordering via Method ID
. Calling a function at runtime will be cheaper if the function is positioned earlier in the order (has a relatively lower Method ID) because 22 gas
are added to the cost of a function for every position that came before it. The caller can save on gas if you prioritize most called functions.
Context: All Contracts
Recommendation:
Find a lower method ID
name for the most called functions for example Call() vs. Call1() is cheaper by 22 gas
For example, the function IDs in the SmartAccount.sol
contract will be the most used; A lower method ID may be given.
Proof of Consept: https://medium.com/joyso/solidity-how-does-function-name-affect-gas-consumption-in-smart-contract-47d270d8ac92
SmartAccount.sol function names can be named and sorted according to METHOD ID
Sighash | Function Signature ======================== affed0e0 => nonce() ce03fdab => nonce(uint256) b0d691fe => entryPoint() 13af4035 => setOwner(address) 025b22bc => updateImplementation(address) 1b71bb6e => updateEntryPoint(address) f698da25 => domainSeparator() 3408e470 => getChainId() 3d46b819 => getNonce(uint256) 184b9559 => init(address,address,address) 6d5433e6 => max(uint256,uint256) 405c3941 => execTransaction(Transaction,uint256,FeeRefund,bytes) 1bb09224 => handlePayment(uint256,uint256,uint256,uint256,address,address) a18f51e5 => handlePaymentRevert(uint256,uint256,uint256,uint256,address,address) 934f3a11 => checkSignatures(bytes32,bytes,bytes) 37cf6f29 => requiredTxGas(address,uint256,bytes,Enum.Operation) c9f909f4 => getTransactionHash(address,uint256,bytes,Enum.Operation,uint256,uint256,uint256,uint256,address,address,uint256) 8d6a6751 => encodeTransactionData(Transaction,FeeRefund,uint256) a9059cbb => transfer(address,uint256) ac85dca7 => pullTokens(address,address,uint256) b61d27f6 => execute(address,uint256,bytes) 18dfb3c7 => executeBatch(address[],bytes[]) 734cd1e2 => _call(address,uint256,bytes) e8d655cf => execFromEntryPoint(address,uint256,bytes,Enum.Operation,uint256) be484bf7 => _requireFromEntryPointOrOwner() ba74b602 => _validateAndUpdateNonce(UserOperation) 0f4cd016 => _validateSignature(UserOperation,bytes32,address) c399ec88 => getDeposit() 4a58db19 => addDeposit() 4d44560d => withdrawDepositTo(address,uint256) 01ffc9a7 => supportsInterface(bytes4)
Make sure Solidity’s optimizer is enabled. It reduces gas costs. If you want to gas optimize for contract deployment (costs less to deploy a contract) then set the Solidity optimizer at a low number. If you want to optimize for run-time gas costs (when functions are called on a contract) then set the optimizer to a high number.
Set the optimization value higher than 800 in your hardhat.config.ts file.
30: solidity: { 31: compilers: [ 32: { 33: version: "0.8.12", 34: settings: { 35: optimizer: { enabled: true, runs: 200 }, 36: }, 37: },
#0 - c4-judge
2023-01-22T16:19:58Z
gzeon-c4 marked the issue as grade-a
#1 - c4-judge
2023-01-22T16:26:54Z
gzeon-c4 marked the issue as selected for report
#2 - c4-sponsor
2023-02-09T12:27:10Z
livingrockrises marked the issue as sponsor confirmed