Biconomy - Smart Contract Wallet contest - 0xSmartContract's results

One-Stop solution to enable an effortless experience in your dApp to onboard new users and abstract away transaction complexities.

General Information

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

Biconomy

Findings Distribution

Researcher Performance

Rank: 7/105

Findings: 3

Award: $1,208.86

QA:
grade-a
Gas:
grade-a

🌟 Selected for report: 2

🚀 Solo Findings: 0

Findings Information

Labels

2 (Med Risk)
satisfactory
duplicate-261

Awards

44.8261 USDC - $44.83

External Links

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

Awards

469.3187 USDC - $469.32

Labels

bug
grade-a
QA (Quality Assurance)
selected for report
sponsor confirmed
edited-by-warden
Q-40

External Links

Summary

Low Risk Issues List

NumberIssues DetailsContext
[L-01]Prevent division by 01
[L-02]Use of EIP 4337, which is likely to change, not recommended for general use or application1
[L-03]Consider using OpenZeppelin's SafeCast library to prevent unexpected overflows when casting from uint2561
[L-04]Gas griefing/theft is possible on unsafe external call8
[L-05]Front running attacks by the onlyOwner1
[L-06]A single point of failure14
[L-07]Loss of precision due to rounding1
[L-08]No Storage Gap for BaseSmartAccount and ModuleManager 2
[L-09]Missing Event for critical parameters init and change1
[L-10]Use 2StepSetOwner instead of setOwner1
[L-11]init() function can be called by anybody1
[L-12]The minimum transaction value of 21,000 gas may change in the future1

Total 12 issues

Non-Critical Issues List

NumberIssues DetailsContext
[N-01]Insufficient coverage1
[N-02]Unused function parameter and local variable2
[N-03]Initial value check is missing in Set Functions3
[N-04]NatSpec comments should be increased in contractsAll Contracts
[N-05]Function writing that does not comply with the Solidity Style GuideAll Contracts
[N-06]Add a timelock to critical functions1
[N-07]For modern and more readable code; update import usages116
[N-08]Include return parameters in NatSpec commentsAll Contracts
[N-09]Long lines are not suitable for the ‘Solidity Style Guide’9
[N-10]Need Fuzzing test23
[N-11]Test environment comments and codes should not be in the main version1
[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 Events1
[N-15]Open TODOs1
[N-16]Mark visibility of init(...) functions as external1
[N-17]Use underscores for number literals2
[N-18]Empty blocks should be removed or Emit something2
[N-19]Use require instead of assert2
[N-20]Implement some type of version counter that will be incremented automatically for contract upgrades1
[N-21]Tokens accidentally sent to the contract cannot be recovered1
[N-22]Use a single file for all system-wide constants10
[N-23]Assembly Codes Specific – Should Have Comments72

Total 23 issues

Suggestions

NumberSuggestion Details
[S-01]Project Upgrade and Stop Scenario should be
[S-02]Use descriptive names for Contracts and Libraries

Total2 suggestions

[L-01] Prevent division by 0

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.

SmartAccount.sol#L247-L295


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:     }

[L-02] Use of EIP 4337, which is likely to change, not recommended for general use or application

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

[L-03] Consider using OpenZeppelin's SafeCast library to prevent unexpected overflows when casting from uint256


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.

[L-04] Gas griefing/theft is possible on unsafe external call

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

https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L451

contracts\smart-contract-wallet\SmartAccount.sol:
247     function handlePayment(
261:        (bool success,) = receiver.call{value: payment}("");
262         require(success, "BSA011");

https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L261

 271     function handlePaymentRevert(
  285:             (bool success,) = receiver.call{value: payment}("");
  286             require(success, "BSA011");

https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L285

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     }

https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L527

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     }

https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol#L37

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     }

https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/StakeManager.sol#L106

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     }

https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/StakeManager.sol#L120

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);

https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/BaseSmartAccount.sol#L108

[L-05] Front running attacks by the 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.

[L-06] A single point of failure

Impact

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

Tools Used

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

[L-07] Loss of precision due to rounding

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);

[L-08] No Storage Gap for BaseSmartAccount and ModuleManager

BaseSmartAccount.sol#L33 ModuleManager.sol#L9

Impact

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;

[L-09] Missing Event for critical parameters init and change

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

[L-10] Use 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.

[L-11] init() function can be called by anybody

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();
        }

[L-12] The minimum transaction value of 21,000 gas may change in the future

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.

protocol_params.go#L31

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;
    }

[N-01] Insufficient coverage

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%.

[N-02] Unused function parameter and local variable


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);

[N-03] Initial value check is missing in Set Functions

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

[N-04] NatSpec comments should be increased in contracts

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

[N-05] 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

[N-06] Add a timelock to critical functions

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 

[N-07] For modern and more readable code; update import usages

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";

[N-08] Include return parameters in NatSpec comments

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);

[N-09] Long lines are not suitable for the ‘Solidity Style Guide’

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
);

[N-10] Need Fuzzing test

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

[N-11] Test environment comments and codes should not be in the main version


1 result - 1 file

contracts/smart-contract-wallet/paymasters/verifying/singleton/VerifyingSingletonPaymaster.sol:
  10: // import "../samples/Signatures.sol";

[N-12] Use of bytes.concat() instead of abi.encodePacked()

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(,)

[N-13] For functions, follow Solidity standard naming conventions (internal function style rule)

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)

[N-14] Omissions in Events

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:     }

[N-15] Open TODOs

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.

[N-16] Mark visibility of init(...) functions as 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

[N-17] Use underscores for number literals

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.

[N-18] Empty blocks should be removed or Emit something

Description: 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.

[N-19] Use 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".

[N-20] Implement some type of version counter that will be incremented automatically for contract upgrades

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:     }

[N-21] Tokens accidentally sent to the contract cannot be recovered

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;
  }
}

[N-22] Use a single file for all system-wide constants

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;

[N-23] Assembly Codes Specific – Should Have Comments

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 {

[S-01] Project Upgrade and Stop Scenario should be

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

[S-02] Use descriptive names for Contracts and Libraries

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

Awards

694.7104 USDC - $694.71

Labels

bug
G (Gas Optimization)
grade-a
selected for report
sponsor confirmed
G-07

External Links

Gas Optimizations List

NumberOptimization DetailsContext
[G-01]With assembly, .call (bool success) transfer can be done gas-optimized8
[G-02]Remove the initializer modifier1
[G-03]Structs can be packed into fewer storage slots2
[G-04]DepositInfo and PaymasterData structs can be rearranged2
[G-05]Duplicated require()/if() checks should be refactored to a modifier or function5
[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 efficient1
[G-08]Unnecessary computation1
[G-09]Using delete instead of setting info struct to 0 saves gas3
[G-10]Empty blocks should be removed or emit something1
[G-11]Using storage instead of memory for structs/arrays saves gas11
[G-12]Use Shift Right/Left instead of Division/Multiplication3
[G-13]Use constants instead of type(uintx).max3
[G-14]Add unchecked {} for subtractions where the operands cannot underflow because of a previous require or if statement2
[G-15]Usage of uints/ints smaller than 32 bytes (256 bits) incurs overhead8
[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 combinations5
[G-19]Functions guaranteed to revert_ when callled by normal users can be marked payable18
[G-20]Setting the constructor to payable4
[G-21]Use assembly to write address storage values8
[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-loops6
[G-23]Sort Solidity operations using short-circuit mode8
[G-24]x += y (x -= y) costs more gas than x = x + y (x = x - y) for state variables3
[G-25]Use a more recent version of solidity36
[G-26]Optimize names to save gas
[G-27]Upgrade Solidity's optimizer

Total 27 issues

[G-01] With assembly, .call (bool success) transfer can be done gas-optimized

return 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

https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L451

contracts\smart-contract-wallet\SmartAccount.sol:
247     function handlePayment(
261:        (bool success,) = receiver.call{value: payment}("");
262         require(success, "BSA011");

https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L261

  271     function handlePaymentRevert(
  285:             (bool success,) = receiver.call{value: payment}("");
  286             require(success, "BSA011");

https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L285

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     }

https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L527

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     }

https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol#L37

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     }

https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/StakeManager.sol#L106

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     }

https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/StakeManager.sol#L120

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);

https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/BaseSmartAccount.sol#L108

[G-02] Remove the initializer modifier

if 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!

[G-03] Structs can be packed into fewer storage slots

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     }

[G-04] DepositInfo and PaymasterData structs can be rearranged

Gas 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: }

[G-05] Duplicated require()/if() checks should be refactored to a modifier or function

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

EntryPoint.sol#L448

contracts\smart-contract-wallet\aa-4337\interfaces\UserOperation.sol:
  49:     if (maxFeePerGas == maxPriorityFeePerGas) {

UserOperation.sol#L49

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.

[G-06] Can be removed to 'assert' in function '_setImplementation'

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     }

https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/common/Singleton.sol#L13

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 CostDeployment Size ┆       ┆        ┆       ┆         │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
71123387             ┆       ┆        ┆       ┆         │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
Function Name                        ┆ min             ┆ avg   ┆ median ┆ max   ┆ # calls │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ _setImplementation                   ┆ 224352243522435224351╰──────────────────────────────────────┴─────────────────┴───────┴────────┴───────┴─────────╯
╭──────────────────────────────────────┬─────────────────┬───────┬────────┬───────┬─────────╮
│ src/test/test.sol:Contract1 contract ┆                 ┆       ┆        ┆       ┆         │
╞══════════════════════════════════════╪═════════════════╪═══════╪════════╪═══════╪═════════╡
Deployment CostDeployment Size ┆       ┆        ┆       ┆         │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
38893225             ┆       ┆        ┆       ┆         │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
Function Name                        ┆ min             ┆ avg   ┆ median ┆ max   ┆ # calls │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ _setImplementation                   ┆ 223362233622336223361╰──────────────────────────────────────┴─────────────────┴───────┴────────┴───────┴─────────╯

[G-07] Instead of emit ExecutionSuccess and emit ExecutionFailure a single emit Execution is gas efficient

If 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 }

[G-08] Unnecessary computation

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     }

SmartAccount.sol#L468

[G-09] Using delete instead of setting info struct to 0 saves gas

contracts\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;

https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/StakeManager.sol#L102-L104

[G-10] Empty blocks should be removed or emit something

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 {}

https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L550

[G-11] Using storage instead of memory for structs/arrays saves gas

When 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

[G-12] Use Shift Right/Left instead of Division/Multiplication

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;

https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/libs/Math.sol#L36

contracts/smart-contract-wallet/SmartAccount.sol:
- 200:      uint256 startGas = gasleft() + 21000 + msg.data.length * 8;
+ 200:      uint256 startGas = gasleft() + 21000 + msg.data.length << 3;

https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L200

- 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");

https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L224

[G-13] Use constants instead of type(uintx).max

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");

EntryPoint.sol#L397

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

[G-14] Add unchecked {} for subtractions where the operands cannot underflow because of a previous require or if statement

require(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);

StakeManager.sol#L118

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

[G-15] Usage of uints/ints smaller than 32 bytes (256 bits) incurs overhead

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 {

BasePaymaster.sol#L75

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

[G-16] Reduce the size of error messages (Long revert Strings)

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");

SmartAccountFactory.sol#L18

contracts/smart-contract-wallet/libs/MultiSend.sol:
  27:         require(address(this) != multisendSingleton, "MultiSend should only be called via delegatecall");

MultiSend.sol#L27

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()

[G-17] Use 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");

[G-18] Use nested if and, avoid multiple check combinations

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) {

Math.sol#L147

Recomendation Code:

contracts\smart-contract-wallet\aa-4337\core\EntryPoint.sol#L410
- 410:    if (paymasterDeadline != 0 && paymasterDeadline < deadline) {
+         if (paymasterDeadline != 0) {
+           if (paymasterDeadline < deadline) {
+           }
+         } 

[G-19] Functions guaranteed to revert_ when callled by normal users can be marked 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 {

FallbackManager.sol#L26

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)

[G-20] Setting the constructor to 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) {

Proxy.sol#L15

contracts/smart-contract-wallet/SmartAccountFactory.sol:
  17:     constructor(address _baseImpl) {

SmartAccountFactory.sol#L17

contracts/smart-contract-wallet/libs/MultiSend.sol:
  12:     constructor() {

MultiSend.sol#L12

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) {

BasePaymaster.sol#L20

Recommendation: Set the constructor to payable

[G-21] Use assembly to write address storage values

8 results - 4 files:

contracts/smart-contract-wallet/SmartAccountFactory.sol:
  17     constructor(address _baseImpl) {
  19:         _defaultImpl = _baseImpl;

SmartAccountFactory.sol#L19

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     }

BasePaymaster.sol#L25

Recommendation Code:

contracts/smart-contract-wallet/paymasters/BasePaymaster.sol#L25
    function setEntryPoint(IEntryPoint _entryPoint) public onlyOwner {
        assembly {
            sstore(entryPoint.slot, _entryPoint)
        }
    }

[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

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;) {

SmartAccount.sol#L468

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

[G-23] Sort Solidity operations using short-circuit mode

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

[G-24] x += y (x -= y) costs more gas than x = x + y (x = x - y) for state variables

3 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.

[G-25] Use a more recent version of solidity

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;

[G-26] Optimize names to save gas

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)

[G-27] Upgrade Solidity's optimizer

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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter