Platform: Code4rena
Start Date: 04/01/2023
Pot Size: $60,500 USDC
Total HM: 15
Participants: 105
Period: 5 days
Judge: gzeon
Total Solo HM: 1
Id: 200
League: ETH
Rank: 21/105
Findings: 4
Award: $609.55
π Selected for report: 0
π Solo Findings: 0
449.9602 USDC - $449.96
https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol#L93 https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol#L385 https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol#L289
Function reverts if one account or paymaster is not validated, which leads to a waste of time and gas.
EntryPoint.UserOpsPerAggregator() takes in an array of opsPerAggregator in its parameter and loops through each struct. In the function, the function _validatePrepayment() is called which validates the account and paymaster from each opsPerAggregator.
function _validatePrepayment(uint256 opIndex, UserOperation calldata userOp, UserOpInfo memory outOpInfo, address aggregator) private returns (address actualAggregator, uint256 deadline) { uint256 preGas = gasleft(); MemoryUserOp memory mUserOp = outOpInfo.mUserOp; _copyUserOpToMemory(userOp, mUserOp); outOpInfo.userOpHash = getUserOpHash(userOp); // validate all numeric values in userOp are well below 128 bit, so they can safely be added // and multiplied without causing overflow uint256 maxGasValues = mUserOp.preVerificationGas | mUserOp.verificationGasLimit | mUserOp.callGasLimit | userOp.maxFeePerGas | userOp.maxPriorityFeePerGas; require(maxGasValues <= type(uint120).max, "AA94 gas values overflow"); uint256 gasUsedByValidateAccountPrepayment; (uint256 requiredPreFund) = _getRequiredPrefund(mUserOp); (gasUsedByValidateAccountPrepayment, actualAggregator, deadline) = _validateAccountPrepayment(opIndex, userOp, outOpInfo, aggregator, requiredPreFund); //a "marker" where account opcode validation is done and paymaster opcode validation is about to start // (used only by off-chain simulateValidation) numberMarker();
The function then continues to call _validateAccountPrepayment which does another round of checks.
function _validateAccountPrepayment(uint256 opIndex, UserOperation calldata op, UserOpInfo memory opInfo, address aggregator, uint256 requiredPrefund) internal returns (uint256 gasUsedByValidateAccountPrepayment, address actualAggregator, uint256 deadline) { unchecked { uint256 preGas = gasleft(); MemoryUserOp memory mUserOp = opInfo.mUserOp; address sender = mUserOp.sender; _createSenderIfNeeded(opIndex, opInfo, op.initCode); if (aggregator == SIMULATE_FIND_AGGREGATOR) { numberMarker(); if (sender.code.length == 0) { // it would revert anyway. but give a meaningful message revert FailedOp(0, address(0), "AA20 account not deployed"); } if (mUserOp.paymaster != address(0) && mUserOp.paymaster.code.length == 0) { // it would revert anyway. but give a meaningful message revert FailedOp(0, address(0), "AA30 paymaster not deployed"); } try IAggregatedAccount(sender).getAggregator() returns (address userOpAggregator) { aggregator = actualAggregator = userOpAggregator; } catch { aggregator = actualAggregator = address(0); } }
If any opsPerAggregator is not validated, the whole function will revert, leading to a waste of time and gas.
VSCode
Make the loop non-atomic by using a try/catch block. If the paymaster / account is not validated, skip the loop and validate the next one. In _compensate, make sure to count the collected amount correctly if some opsPerAggregator are skipped because of validation failure.
#0 - c4-judge
2023-01-18T00:25:32Z
gzeon-c4 marked the issue as duplicate of #499
#1 - c4-sponsor
2023-02-08T08:13:49Z
livingrockrises marked the issue as sponsor acknowledged
#2 - c4-judge
2023-02-10T12:20:47Z
gzeon-c4 marked the issue as satisfactory
π Selected for report: immeas
Also found by: 0xDave, 0xbepresent, HE1M, Kutu, betweenETHlines, hansfriese, hihen, peanuts, prc, wait
78.2598 USDC - $78.26
https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L460 https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L465 https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L494
EntryPoint address in SmartAccount.sol cannot execute certain functions because of the onlyOwner modifier.
The functions SmartAccount.execute() and SmartAccount.executeBatch() has an onlyOwner modifier.
function execute(address dest, uint value, bytes calldata func) external onlyOwner{ function executeBatch(address[] calldata dest, bytes[] calldata func) external onlyOwner{
However, both functions call another function _requireFromEntryPointOrOwner();
_requireFromEntryPointOrOwner();
which checks if msg.sender is adderss(entryPoint()) or owner
function _requireFromEntryPointOrOwner() internal view { require(msg.sender == address(entryPoint()) || msg.sender == owner, "account: not Owner or EntryPoint"); }
Since execute() and executeBatch() has an onlyOwner modifier which checks if msg.sender is owner only,
modifier onlyOwner { require(msg.sender == owner, "Smart Account:: Sender is not authorized"); _; }
address(entryPoint()) cannot call the function execute() and executeBatch().
VSCode
Either take out the onlyOwner modifer in the two functions or remove the _requireFromEntryPointOrOwner() check if the function intends to not allow the msg.sender to be address(entryPoint()).
#0 - c4-judge
2023-01-18T00:38:45Z
gzeon-c4 marked the issue as duplicate of #390
#1 - c4-sponsor
2023-01-26T06:53:00Z
livingrockrises marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-10T12:21:33Z
gzeon-c4 marked the issue as satisfactory
π Selected for report: 0x52
Also found by: 0xSmartContract, Deivitto, Diana, IllIllI, Koolex, Rolezn, SleepingBugs, V_B, adriro, betweenETHlines, cryptostellar5, oyc_109, peanuts
44.8261 USDC - $44.83
Protocol cannot upgrade the upgradeable contract.
SmartAccount.sol uses ReentrancyGuardUpgradeable but not UUPSUpgradeable.
contract SmartAccount is Singleton, BaseSmartAccount, IERC165, ModuleManager, SignatureDecoder, SecuredTokenTransfer, ISignatureValidatorConstants, FallbackManager, Initializable, ReentrancyGuardUpgradeable {
Manual Review
import UUPSUpgradeable, inherit the contract, and initialize the contract
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract SmartAccount is Singleton, BaseSmartAccount, IERC165, ModuleManager, SignatureDecoder, SecuredTokenTransfer, ISignatureValidatorConstants, FallbackManager, Initializable, ReentrancyGuardUpgradeable + UUPSUpgradeable {
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"); + __UUPSUpgradeable_init(); owner = _owner; _entryPoint = IEntryPoint(payable(_entryPointAddress)); if (_handler != address(0)) internalSetFallbackHandler(_handler); setupModules(address(0), bytes("")); }
Also add storage gap for upgradeable contracts.
uint256[45] private __gap;
#0 - c4-judge
2023-01-17T07:54:51Z
gzeon-c4 marked the issue as primary issue
#1 - c4-sponsor
2023-01-19T17:11:22Z
livingrockrises marked the issue as sponsor confirmed
#2 - livingrockrises
2023-01-19T17:44:39Z
someone also mentioned that Initializable should also come from open zeppelin upgrades library. https://github.com/code-423n4/2023-01-biconomy-findings/issues/540
#3 - c4-judge
2023-02-10T12:36:39Z
gzeon-c4 marked the issue as satisfactory
#4 - c4-judge
2023-02-10T12:39:19Z
gzeon-c4 marked issue #261 as primary and marked this issue as a duplicate of 261
π Selected for report: 0xSmartContract
Also found by: 0x1f8b, 0xAgro, 0xdeadbeef0x, 0xhacksmithh, 2997ms, Atarpara, Bnke0x0, Diana, HE1M, IllIllI, Josiah, Kalzak, Lirios, MalfurionWhitehat, MyFDsYours, Raiders, RaymondFam, Rolezn, SaharDevep, Sathish9098, Udsen, Viktor_Cortess, adriro, ast3ros, betweenETHlines, btk, chaduke, chrisdior4, cryptostellar5, csanuragjain, giovannidisiena, gz627, hl_, horsefacts, joestakey, juancito, ladboy233, lukris02, nadin, oyc_109, pauliax, peanuts, prady, sorrynotsorry, zaskoh
36.5015 USDC - $36.50
If the initializer is not executed in the same transaction as the constructor, a malicious user can front-run the initialize() call, forcing the contract to be redeployed.
Setting the owner within one function can be dangerous if the address of the owner in the parameter is not set properly. Consider using a two-step process whereby the owner has to acknowledge the change before changing the ownership.
If the intention is for the Ether to be used, the function should call another function, otherwise it should revert (e.g. require(msg.sender == address(weth)). Having no access control on the function means that someone may send Ether to the contract, and have no way to get anything back out, which is a loss of funds
Use OpenZeppelinβs ECDSA contract rather than calling ecrecover() directly
Example:
import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
instead of
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
Applies to all contracts in Biconomy protocol.
SmartAccount.sol:
import "./libs/LibAddress.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import "./BaseSmartAccount.sol"; import "./common/Singleton.sol"; import "./base/ModuleManager.sol"; import "./base/FallbackManager.sol"; import "./common/SignatureDecoder.sol"; import "./common/SecuredTokenTransfer.sol"; import "./interfaces/ISignatureValidator.sol"; import "./interfaces/IERC165.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
eg. 0.8.10 instead of ^0.8.10
Use allowlist rather than whitelist.
What is the overall line coverage percentage provided by your tests?: 41
Protocol should aim to achieve >95% testing coverage to make sure that all functions are in working order.
#0 - c4-judge
2023-01-22T15:40:58Z
gzeon-c4 marked the issue as grade-b
#1 - c4-sponsor
2023-02-09T12:28:06Z
livingrockrises marked the issue as sponsor confirmed