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: 28/105
Findings: 3
Award: $426.03
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: adriro
Also found by: 0x1f8b, 0x73696d616f, 0xdeadbeef0x, BClabs, HE1M, Haipls, Jayus, Kalzak, Lirios, Qeew, V_B, adriro, ast3ros, aviggiano, betweenETHlines, bin2chen, chaduke, dragotanqueray, ey88, giovannidisiena, hihen, horsefacts, ladboy233, wait, zaskoh
26.2582 USDC - $26.26
The salt
used for create2 does not include information from the init
method, so it is vulnerable to front-running.
it's impossible to override an existing contract in Ethereum. From EIP-684:
If a contract creation is attempted, due to either a creation transaction or the CREATE (or future CREATE2) opcode, and the destination address already has either nonzero nonce, or nonempty code, then the creation throws immediately, with exactly the same behavior as would arise if the first byte in the init code were an invalid opcode. This applies retroactively starting from genesis.
For that reason if the salt is repeated, the call fill fault.
bytes32 salt = keccak256(abi.encodePacked(_owner, address(uint160(_index))));
If the user want to deploy a wallet using deployCounterFactualWallet
with the _owner=A
and _index=0
, an attacker can listen the memory pool and deploy with an higer gas, a wallet with the same owner and index, but with different _entryPoint
and _handler
so the attacker can deny the original call, and make unwanted actions in the proxy.
BaseSmartAccount(proxy).init(_owner, _entryPoint, _handler);
Include all received fields in the salt calculation.
#0 - c4-judge
2023-01-17T07:22:36Z
gzeon-c4 marked the issue as duplicate of #460
#1 - c4-sponsor
2023-01-26T03:18:28Z
livingrockrises marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-10T12:25:11Z
gzeon-c4 marked the issue as satisfactory
#3 - c4-judge
2023-02-10T12:25:24Z
gzeon-c4 changed the severity to 3 (High Risk)
🌟 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
361.0144 USDC - $361.01
abstract
for base contractsabstract
contracts are contracts that have at least one function without its implementation. An instance of an abstract cannot be created.
Reference:
Affected source code:
The following ones should be libraries instead of contracts:
The pragma version used are:
pragma solidity 0.8.12;
Note that mixing pragma is not recommended. Because different compiler versions have different meanings and behaviors, it also significantly raises maintenance costs. As a result, depending on the compiler version selected for any given file, deployed contracts may have security issues.
The minimum required version must be 0.8.17; otherwise, contracts will be affected by the following important bug fixes:
abi.encodeCall
in place of fixed bytes arguments.calldatasize()
in all cases.bytes
arrays.Apart from these, there are several minor bug fixes and improvements.
paymasterContext
The method comment states:
Encodes the paymaster context: sender, token, rate, and fee
However, only paymasterId
is encoded.
/** * @dev Encodes the paymaster context: sender, token, rate, and fee */ function paymasterContext( UserOperation calldata op, PaymasterData memory data ) internal pure returns (bytes memory context) { return abi.encode(data.paymasterId); }
Affected source code:
The following methods have a lack of checks if the received argument is an address, it's good practice in order to reduce human error to check that the address specified in the constructor or initialize is different than address(0)
.
Affected source code:
It doesn't check for range of _unstakeDelaySec
and can leave locked forever with high time, it also doesn't check that msg.value
is greater than 0, so a second call with 0, works and could not be expected.
The owner can change the address of _entryPoint
and affect user deposits when they call the deposit
method (for example with front-running), thus requiring an unnecessary trust in the project to use.
Affected source code:
There are a lack of checks during the constructor that allows an empty _implementation
, fallback
works with target = address(0)
and the receive
method does not call target
, so tokens could be lost if it's called.
fallback() external payable { address target; // solhint-disable-next-line no-inline-assembly assembly { target := sload(_IMPLEMENTATION_SLOT) calldatacopy(0, 0, calldatasize()) let result := delegatecall(gas(), target, 0, calldatasize(), 0, 0) returndatacopy(0, 0, returndatasize()) switch result case 0 {revert(0, returndatasize())} default {return (0, returndatasize())} } }
If the proxy is deployed with an empty address for implementation
, the token will be lost.
Affected source code:
Because the code is inside an unchecked
block, overflows can occur. If the call to innerHandleOp
uses a high opInfo.preOpGas
, actualGas
might overflow the _handlePostOp
method when it is in an unchecked
region.
Affected source code:
If the user set the address of StakeManager
as withdrawAddress
.
(bool success,) = withdrawAddress.call{value : withdrawAmount}("");
The sender will be StakeManager
and the tokens are lost forever because it will be deposited as the contract himself.
receive() external payable { depositTo(msg.sender); }
Affected source code:
The code that contains "open todos" reflects that the development is not finished and that the code can change a posteriori, prior release, with or without audit.
Affected source code:
TODO: copy logic of gasPrice?
The contract Math
has the following comment:
Return the log in base 10, following the selected rounding direction, of a positive value.
But this is not true, because is base 256 as was fixed in https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3916.
Affected source code:
It's possible to lose the ownership under specific circumstances.
Because of human error it's possible to set a new invalid owner. When you want to change the owner's address it's better to propose a new owner, and then accept this ownership with the new wallet.
Affected source code:
The following low level calls are insecure since they do not verify that the calling address is a contract and not an empty address or an EOA. For that reason, these methods are prone to phantom methods attacks.
Proof of Concept:
If you try to call the following methods with the following contract 0x000000000000000000000000000000000000000000000
(or any EOA) you can see that the transaction is successful, you have to check that the address is a valid contract and maybe check that the call returns a certain data. Otherwise it is possible to call it and lose tokens or produce undesired errors.
This remind me to this attack https://certik.medium.com/qubit-bridge-collapse-exploited-to-the-tune-of-80-million-a7ab9068e1a0 where we can see:
Affected source code:
Authoritative internal methods must use '_
' before the name. We have an example in Singleton.sol#L12 where the methods that require authorization but are internal, a different naming is established.
Affected source code:
It is not good practice to hardcode values, but if you are dealing with addresses much less, these can change between implementations, networks or projects, so it is convenient to remove these values from the source code.
Affected source code:
Use the selector instead of the raw value:
#0 - c4-judge
2023-01-22T15:51:16Z
gzeon-c4 marked the issue as grade-a
#1 - c4-sponsor
2023-02-09T12:24:14Z
livingrockrises marked the issue as sponsor confirmed
🌟 Selected for report: 0xSmartContract
Also found by: 0x1f8b, 0xhacksmithh, Aymen0909, Bnke0x0, IllIllI, Rageur, RaymondFam, Rickard, Rolezn, Secureverse, arialblack14, chaduke, chrisdior4, cthulhu_cult, giovannidisiena, gz627, lukris02, oyc_109, pavankv, privateconstant, shark
38.7634 USDC - $38.76
The following state variables can be removed without affecting the logic of the contract since they are not used and/or are redundant because they could be used inline.
Affected source code:
internalIncrementDeposit
It's possible to optimize the internalIncrementDeposit
method like follows:
- function internalIncrementDeposit(address account, uint256 amount) internal { + function internalIncrementDeposit(address account, uint256 amount) internal returns (uint112 ret) { DepositInfo storage info = deposits[account]; uint256 newAmount = info.deposit + amount; require(newAmount <= type(uint112).max, "deposit overflow"); + ret = uint112(newAmount); + info.deposit = ret; - info.deposit = uint112(newAmount); } function depositTo(address account) public payable { - internalIncrementDeposit(account, msg.value); + emit Deposited(account, internalIncrementDeposit(account, msg.value)); - DepositInfo storage info = deposits[account]; - emit Deposited(account, info.deposit); }
Gas diff:
In red the old version, in green with the applied changes:
- rcpt.gasUsed= 155168 0x31494a79c7dcc9163be9fc3c9f79c43547e4b39050c8b14920f02a5bcb97588d + rcpt.gasUsed= 154996 0x40cc316df0a2fb9d6261915aea960f905dd63e9da797f3d5e24b0fe68149c1c3 - == actual gasUsed (from tx receipt)= 155168 + == actual gasUsed (from tx receipt)= 154996 ... - == actual gasUsed (from tx receipt)= 383762 + == actual gasUsed (from tx receipt)= 383578 - == calculated gasUsed (paid to beneficiary)= BigNumber { value: "334787" } + == calculated gasUsed (paid to beneficiary)= BigNumber { value: "334599" }
Affected source code:
unlockStake
You can use withdrawTime != 0
as a staked
flag, so you could remove that field from the DepositInfo
structure, saving space and gas.
Affected source code:
The following structures could be optimized moving the position of certain values in order to save some storage slots:
struct Transaction { address to; + Enum.Operation operation; uint256 value; bytes data; - Enum.Operation operation; uint256 targetTxGas; }
Reference:
Enums are represented by integers; the possibility listed first by 0, the next by 1, and so forth. An enum type just acts like uintN, where N is the smallest legal value large enough to accomodate all the possibilities.
Gas diff:
In red the old version, in green with the applied changes:
mint tokens to owner address.. { to: '0x0ed64d01D0B4B655E410EF1441dD677B695639E7', value: 0, data: '0xa9059cbb0000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000000000000000000000000000008ac7230489e80000', operation: 0, targetTxGas: 0, baseGas: 0, gasPrice: 0, tokenGasPriceFactor: 1, gasToken: '0x0000000000000000000000000000000000000000', refundReceiver: '0x0000000000000000000000000000000000000000', nonce: BigNumber { value: "0" } } - estimated gas to be used 102719 + estimated gas to be used 102710 - Real txn gas used: 98875 + Real txn gas used: 98866 targetTxGas estimation part 1: 36544 - estimated gas to be used 135491 + estimated gas to be used 135482 { - baseGas: 103947, + baseGas: 103938,
Affected source code:
unchecked
keywordWhen an underflow or overflow cannot occur, one might conserve gas by using the unchecked
keyword to prevent unnecessary arithmetic underflow/overflow tests.
function simulateValidation(UserOperation calldata userOp) external { uint256 preGas = gasleft(); UserOpInfo memory outOpInfo; (address aggregator, uint256 deadline) = _validatePrepayment(0, userOp, outOpInfo, SIMULATE_FIND_AGGREGATOR); uint256 prefund = outOpInfo.prefund; - uint256 preOpGas = preGas - gasleft() + userOp.preVerificationGas; + uint256 preOpGas; + unchecked { + preOpGas = preGas - gasleft() + userOp.preVerificationGas; + } StakeInfo memory paymasterInfo = getStakeInfo(outOpInfo.mUserOp.paymaster); StakeInfo memory senderInfo = getStakeInfo(outOpInfo.mUserOp.sender); bytes calldata initCode = userOp.initCode; address factory = initCode.length >= 20 ? address(bytes20(initCode[0 : 20])) : address(0); StakeInfo memory factoryInfo = getStakeInfo(factory); if (aggregator != address(0)) { AggregatorStakeInfo memory aggregatorInfo = AggregatorStakeInfo(aggregator, getStakeInfo(aggregator)); revert SimulationResultWithAggregation(preOpGas, prefund, deadline, senderInfo, factoryInfo, paymasterInfo, aggregatorInfo); } revert SimulationResult(preOpGas, prefund, deadline, senderInfo, factoryInfo, paymasterInfo); }
Gas diff:
In red the old version, in green with the applied changes:
√ legacy mode (maxPriorityFee==maxFeePerGas) should not use "basefee" opcode - == est gas= 114321 + == est gas= 114297 - rcpt.gasUsed= 111704 0x914c39060be62a5980f0ffa05b56214fc3301dbc44b7ba872afd0753058b95ca + rcpt.gasUsed= 111680 0xa023ccb107b9dc80adcb3d031d48664146215e76e030839f0d8033f100d44faf - == actual gasUsed (from tx receipt)= 111704 + == actual gasUsed (from tx receipt)= 111680 create account √ should reject if account not funded - == create gasUsed= 381974 + == create gasUsed= 381962 - == actual gasUsed (from tx receipt)= 381974 + == actual gasUsed (from tx receipt)= 381962 - | [90mEntryPoint[39m · handleAggregatedOps · [36m163341[39m · [31m345337[39m · 245824 · [90m6[39m · [32m[90m-[32m[39m │ + | [90mEntryPoint[39m · handleAggregatedOps · [36m163341[39m · [31m345337[39m · 245822 · [90m6[39m · [32m[90m-[32m[39m │ - | [90mEntryPoint[39m · handleOps · [36m101633[39m · [31m507641[39m · 256369 · [90m28[39m · [32m[90m-[32m[39m │ + | [90mEntryPoint[39m · handleOps · [36m101597[39m · [31m507653[39m · 256364 · [90m28[39m · [32m[90m-[32m[39m │
Affected source code:
#0 - c4-judge
2023-01-22T16:21:34Z
gzeon-c4 marked the issue as grade-b
#1 - c4-sponsor
2023-02-09T12:25:09Z
livingrockrises marked the issue as sponsor confirmed