Biconomy - Smart Contract Wallet contest - shark'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: 70/105

Findings: 1

Award: $38.76

Gas:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

38.7634 USDC - $38.76

Labels

bug
G (Gas Optimization)
grade-b
sponsor confirmed
edited-by-warden
G-12

External Links

1. Splitting require statements that use && saves gas

Instead of using the && operator to check multiple conditions, use multiple require() statements. This will save approximately 3 gas per &&.

Here is an example of this issue:

File: ModuleManager.sol Line 34

        require(module != address(0) && module != SENTINEL_MODULES, "BSA101");

In the example above, the && could be split up:

        require(module != address(0), "BSA101");
        require(module != SENTINEL_MODULES, "BSA101");

Here are 2 other instances of this issue:

2. Strict equalities cost less gas than non-strict

Strict equalities (<, >) cost less gas than non-strict (<=, >=). This is because strict equality uses fewer opcodes.

For instance:

File: EntryPoint.sol Line 213

            require(paymasterAndData.length >= 20, "AA93 invalid paymasterAndData");

The require statement above could use > instead of >= to save gas:

            require(paymasterAndData.length > 19, "AA93 invalid paymasterAndData");

Here are some more instances of this issue:

3. Functions with access control marked as payable cost less gas

Functions with access control will cost less gas when marked as payable (assuming the caller has correct permissions). This is because the compiler doesn't need to check for msg.value, which saves approximately 20 gas per call.

Consider adding payable to the following functions to save gas:

4. Useless if statement

File: SmartAccount.sol Line 166 - 176

166    function init(address _owner, address _entryPointAddress, address _handler) public override initializer {
167        require(owner == address(0), "Already initialized");
168        require(address(_entryPoint) == address(0), "Already initialized");
169        require(_owner != address(0),"Invalid owner");
170        require(_entryPointAddress != address(0), "Invalid Entrypoint");
171        require(_handler != address(0), "Invalid Entrypoint");
172        owner = _owner;
173        _entryPoint =  IEntryPoint(payable(_entryPointAddress));
174        if (_handler != address(0)) internalSetFallbackHandler(_handler);
175        setupModules(address(0), bytes(""));
176    }

In the function above, the if statement (line 174) is checking the same thing as line 171, rendering it redundant.

As such, line 174 may be refactored to omit the if statement:

174 internalSetFallbackHandler(_handler);

5. x += y costs more gas than x = x + y (same for -=)

Using the addition operator instead of plus equals saves around 22 gas

Here are some instances of this issue:

6. Unchecked maths

Use the unchecked keyword to avoid unnecessary arithmetic checks and save gas when an underflow/overflow will not happen.

For instance:

File: EntryPoint.sol Line 100-102

        for (uint256 i = 0; i < opasLen; i++) {
            totalOps += opsPerAggregator[i].userOps.length;
        }

Since i is constrained by opasLen (a uint256), overflowing will never happen here.

As such, i++ may be unchecked to save gas:

        for (uint256 i = 0; i < opasLen;) {
            totalOps += opsPerAggregator[i].userOps.length;
            unchecked { i++; }
        }

7. Unnecessary cast into a uint256()

Casting a number into a uint256() is unnecessary if the number is already a uint256.

File: SmartAccount.sol Line 322

                require(uint256(s) >= uint256(1) * 65, "BSA021");

uint256(1) is redundant as 1 is already a uint256. Moreover, 1 * 65 is equal to 65 so 1 may be omitted altogether:

                require(uint256(s) >= 65, "BSA021");

8. Using storage instead of memory for structs/arrays saves gas

When retrieving data from a storage location, assigning the data to a memory variable will cause all fields of the struct/array to be read from storage, which incurs a Gcoldsload (2100 gas) for each field of the struct/array. If the fields are read from the new memory variable, they incur an additional MLOAD rather than a cheap stack read. Instead of declaring the variable with the memory keyword, declaring the variable with the storage keyword and caching any fields that need to be re-read in stack variables, will be much cheaper, only incurring the Gcoldsload for the fields actually read. The only time it makes sense to read the whole struct/array into a memory variable, is if the full struct/array is being returned by the function, is being passed to a function that requires memory, or if the array/struct is being read from another memory array/struct

File: EntryPoint.sol (Line 179, Line 229, Line 234, Line 235)

171:        MemoryUserOp memory mUserOp = opInfo.mUserOp;

229:        UserOpInfo memory outOpInfo;

234:        StakeInfo memory paymasterInfo = getStakeInfo(outOpInfo.mUserOp.paymaster);
235:        StakeInfo memory senderInfo = getStakeInfo(outOpInfo.mUserOp.sender);

#0 - c4-judge

2023-01-22T16:23:39Z

gzeon-c4 marked the issue as grade-b

#1 - c4-sponsor

2023-02-09T12:32:43Z

livingrockrises marked the issue as sponsor confirmed

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