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: 2/105
Findings: 10
Award: $7,414.34
π Selected for report: 3
π Solo Findings: 0
π Selected for report: V_B
Also found by: 0xdeadbeef0x, HE1M, Koolex, Matin, adriro, chaduke, gogo, hihen, jonatascm, kankodu, ro, smit_rajput, spacelord47, taek
163.1671 USDC - $163.17
https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L166 https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L192 https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L229 https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/base/Executor.sol#L23
If the SmartAccount
implementation contract is not initialized, it can be destroyed using the following attack scenario:
SmartAccount
implementation contract using the init
function.delegatecall
to a contract that executes the selfdestruct
opcode on any incoming call, such as:contract Destructor { fallback() external { selfdestruct(payable(0)); } }
The destruction of the implementation contract would result in the freezing of all functionality of the wallets that point to such an implementation. It would also be impossible to change the implementation address, as the Singleton
functionality and the entire contract would be destroyed, leaving only the functionality from the Proxy contract accessible.
In the deploy script there is the following logic:
const SmartWallet = await ethers.getContractFactory("SmartAccount"); const baseImpl = await SmartWallet.deploy(); await baseImpl.deployed(); console.log("base wallet impl deployed at: ", baseImpl.address);
So, in the deploy script there is no enforce that the SmartAccount
contract implementation was initialized.
The same situation in scw-contracts/scripts/wallet-factory.deploy.ts
script.
Please note, that in case only the possibility of initialization of the SmartAccount
implementation will be banned it will be possible to use this attack. This is so because in such a case owner
variable will be equal to zero and it will be easy to pass a check inside of checkSignatures
function using the fact that for incorrect input parameters ecrecover
returns a zero address.
Complete freezing of all functionality of all wallets (including complete funds freezing).
Add to the deploy script initialization of the SmartAccount
implementation, or add to the SmartAccount
contract the following constructor that will prevent implementation contract from the initialization:
// Constructor ensures that this implementation contract can not be initialized constructor() public { owner = address(1); }
#0 - c4-judge
2023-01-17T06:45:01Z
gzeon-c4 marked the issue as primary issue
#1 - c4-judge
2023-01-17T07:00:35Z
gzeon-c4 marked the issue as satisfactory
#2 - gzeoneth
2023-01-17T07:13:25Z
#14 also note that if owner is left to address(0) some validation can be bypassed
#3 - c4-sponsor
2023-01-25T22:46:14Z
livingrockrises marked the issue as sponsor confirmed
#4 - livingrockrises
2023-01-25T23:41:08Z
#6 is not duplicate of this issue
#5 - livingrockrises
2023-01-25T23:47:36Z
#43 is also not duplicate of this issue
#6 - c4-judge
2023-02-10T12:28:55Z
gzeon-c4 marked the issue as selected for report
4814.3885 USDC - $4,814.39
https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L200 https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L239 https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L248
The execTransaction
function is designed to accept a relayed transaction with a transaction cost refund. At the beginning of the function, the startGas
value is calculated as the amount of gas that the relayer will approximately spend on the transaction initialization, including the base cost of 21000
gas and the cost per calldata byte of msg.data.length * 8
gas. At the end of the function, the total consumed gas is calculated as gasleft() - startGas
and the appropriate refund is sent to the relayer.
An attacker could manipulate the calldata to increase the refund amount while spending less gas than what is calculated by the contract. To do this, the attacker could provide calldata with zero padded bytes of arbitrary length. This would only cost 4 gas per zero byte, but the refund would be calculated as 8 gas per calldata byte. As a result, the refund amount would be higher than the gas amount spent by the relayer.
Letβs a smart wallet user signs a transaction. Some of the relayers trying to execute this transaction and send a transaction to the SmartAccount
contract. Then, an attacker can frontrun the transaction, changing the transaction calldata by adding the zeroes bytes at the end.
So, the original transaction has such calldata:
abi.encodeWithSignature(RelayerManager.execute.selector, (...))
The modified (frontrun) transaction calldata:
// Basically, just add zero bytes at the end abi.encodeWithSignature(RelayerManager.execute.selector, (...)) || 0x00[]
The PoC shows that the function may accept the data with redundant zeroes at the end. At the code above, an attacker send a 100_000 meaningless zeroes bytes, that gives a 100_000 * 4 = 400_000
additional gas refund. Technically, it is possible to pass even more zero bytes.
pragma solidity ^0.8.12; contract DummySmartWallet { function execTransaction( Transaction memory _tx, uint256 batchId, FeeRefund memory refundInfo, bytes memory signatures ) external { // Do nothing, just test that data with appended zero bytes are accepted by Solidity } } contract PoC { address immutable smartWallet; constructor() { smartWallet = address(new DummySmartWallet()); } // Successfully call with original data function testWithOriginalData() external { bytes memory txCalldata = _getOriginalTxCalldata(); (bool success, ) = smartWallet.call(txCalldata); require(success); } // Successfully call with original data + padded zero bytes function testWithModifiedData() external { bytes memory originalTxCalldata = _getOriginalTxCalldata(); bytes memory zeroBytes = new bytes(100000); bytes memory txCalldata = abi.encodePacked(originalTxCalldata, zeroBytes); (bool success, ) = smartWallet.call(txCalldata); require(success); } function _getOriginalTxCalldata() internal pure returns(bytes memory) { Transaction memory transaction; FeeRefund memory refundInfo; bytes memory signatures; return abi.encodeWithSelector(DummySmartWallet.execTransaction.selector, transaction, uint256(0), refundInfo, signatures); } }
An attacker to manipulate the gas refund amount to be higher than the gas amount spent, potentially leading to arbitrary big ether loses by a smart wallet.
You can calculate the number of bytes used by the relayer as a sum per input parameter. Then an attacker won't have the advantage of providing non-standard ABI encoding for the PoC calldata.
// Sum the length of each bynamic and static length parameters. uint256 expectedNumberOfBytes = _tx.data.length + signatures.length + 12 * 32; uint256 dataLen = Math.min(msg.data.length, expectedNumberOfBytes);
Please note, the length of the signature
must also be bounded to eliminate the possibility to put meaningless zeroes there.
#0 - c4-judge
2023-01-17T06:03:37Z
gzeon-c4 marked the issue as duplicate of #535
#1 - c4-judge
2023-01-17T06:04:48Z
gzeon-c4 marked the issue as not a duplicate
#2 - c4-judge
2023-01-17T06:04:52Z
gzeon-c4 marked the issue as primary issue
#3 - c4-judge
2023-01-17T06:06:15Z
gzeon-c4 marked the issue as satisfactory
#4 - c4-sponsor
2023-01-25T06:19:35Z
livingrockrises marked the issue as sponsor confirmed
#5 - livingrockrises
2023-01-26T00:38:11Z
#492 is not a duplicate of this issue
#6 - c4-judge
2023-02-10T12:26:16Z
gzeon-c4 marked the issue as selected for report
π 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 deployCounterFactualWallet
function deploys a smart wallet using the create2
function with a salt value that depends on the _owner
and _index
parameters. However, the address derivation for the deployed wallet does not depend on the _entryPoint
and _handler
parameters, which are used during initialization. This allows an attacker to front run the transaction and deploy a smart contract with their own _handler
and _entryPoint
, but same address. The attacker can then call the execFromEntryPoint
function on the smart wallet, allowing them to execute any desired logic, such as changing the owner. As a result, users cannot rely on the address derivation in advance or deploy the wallet with the same address on different EVM chains.
If users rely on the address derivation in advance or try to deploy the wallet with the same address on different EVM chains, any funds sent to the wallet could potentially be withdrawn by anyone else. All in all, it could lead to the theft of user funds.
Change formula with which salt
is calculated to depends _entryPoint
and _handler
.
#0 - c4-judge
2023-01-17T07:20:56Z
gzeon-c4 marked the issue as duplicate of #460
#1 - c4-sponsor
2023-01-26T03:05:00Z
livingrockrises marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-10T12:25:09Z
gzeon-c4 marked the issue as satisfactory
22.7235 USDC - $22.72
https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L218 https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L342
In the checkSignatures
there are checks that the signer is the account owner, but in the case of EIP-1271 signature check there are no such checks:
// If v is 0 then it is a contract signature // When handling contract signatures the address of the contract is encoded into r _signer = address(uint160(uint256(r))); // Check that signature data pointer (s) is not pointing inside the static part of the signatures bytes // This check is not completely accurate, since it is possible that more signatures than the threshold are send. // Here we only check that the pointer is not pointing inside the part that is being processed require(uint256(s) >= uint256(1) * 65, "BSA021"); // Check that signature data pointer (s) is in bounds (points to the length of data -> 32 bytes) require(uint256(s) + 32 <= signatures.length, "BSA022"); // Check if the contract signature is in bounds: start of data is s + 32 and end is start + signature length uint256 contractSignatureLen; // solhint-disable-next-line no-inline-assembly assembly { contractSignatureLen := mload(add(add(signatures, s), 0x20)) } require(uint256(s) + 32 + contractSignatureLen <= signatures.length, "BSA023"); // Check signature bytes memory contractSignature; // solhint-disable-next-line no-inline-assembly assembly { // The signature data for contract signatures is appended to the concatenated signatures and the offset is stored in s contractSignature := add(add(signatures, s), 0x20) } require(ISignatureValidator(_signer).isValidSignature(data, contractSignature) == EIP1271_MAGIC_VALUE, "BSA024");
So everyone can sign any transaction using the EIP-1271 signature validation method and convince the wallet that the valid signature was verified.
The complete absence of signature verification, and as a result, the possibility of performing any transaction by a third party.
Add the following check into the EIP-1271 signature check logic:
require(_signer == owner, "INVALID_SIGNATURE");
#0 - c4-judge
2023-01-17T06:59:15Z
gzeon-c4 marked the issue as duplicate of #175
#1 - c4-sponsor
2023-01-19T22:07:54Z
livingrockrises marked the issue as sponsor confirmed
#2 - livingrockrises
2023-01-19T22:08:09Z
confirmed duplicate of #175
#3 - c4-judge
2023-02-10T12:28:17Z
gzeon-c4 marked the issue as satisfactory
492.0314 USDC - $492.03
https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L239 https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L264 https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L424
For the calculation of the amount of the token to be paid to the relayer tokenGasPriceFactor
value is used. The corresponding logic is the following:
payment = (gasUsed + baseGas) * (gasPrice) / (tokenGasPriceFactor); require(transferToken(gasToken, receiver, payment), "BSA012");
So, the number of tokens that the relayer should receive is inversely proportional to the value of this variable. But the tokenGasPriceFactor
parameter is not signed by the owner:
bytes32 safeTxHash = keccak256( abi.encode( ACCOUNT_TX_TYPEHASH, _tx.to, _tx.value, keccak256(_tx.data), _tx.operation, _tx.targetTxGas, refundInfo.baseGas, refundInfo.gasPrice, refundInfo.gasToken, refundInfo.refundReceiver, _nonce ) );
Using this fact the relayer can pass any value of tokenGasPriceFactor
parameter to receive a greater amount than the user expected to pay.
The user sends a transaction to the relayer. By the formula, payment
value should be equal gasUsed/tokenGasPriceFactor
. The user expects that the tokenGasPriceFactor
will be equal to 100
for the specified token, but the relayer put tokenGasPriceFactor
to 1
. Relayer receives 100
times greater payout.
Obtaining a greater relayer benefit than the user expects, theft of user funds.
Add the refundInfo.tokenGasPriceFactor
to the preimage of the tx hash which should be signed by the owner.
#0 - c4-judge
2023-01-17T06:15:06Z
gzeon-c4 marked the issue as primary issue
#1 - c4-judge
2023-01-17T06:15:11Z
gzeon-c4 marked the issue as satisfactory
#2 - c4-sponsor
2023-01-25T06:26:44Z
livingrockrises marked the issue as sponsor confirmed
#3 - c4-judge
2023-02-10T12:31:06Z
gzeon-c4 marked the issue as selected for report
#4 - c4-judge
2023-02-10T12:33:23Z
gzeon-c4 marked issue #123 as primary and marked this issue as a duplicate of 123
π Selected for report: Tointer
Also found by: 0xdeadbeef0x, HE1M, Haipls, Koolex, PwnedNoMore, Tricko, V_B, csanuragjain, orion, peakbolt, ro, romand, taek
149.4204 USDC - $149.42
https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L194 https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L212 https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L218
The execTransaction
function includes an input parameter called batchId
that is used to determine the nonce which is included in the data signed by the owner. However, batchId
is not part of the signed data. This allows any third party to replay a signed transaction using a different batchId
value, as the nonce values will be the same.
Possibility of replay of any transaction for which there exists a batchId
with corresponding nonce value.
Add batchId
into Transaction
struct and include its value into the tx hash preimage in encodeTransactionData
function.
#0 - c4-judge
2023-01-17T07:02:16Z
gzeon-c4 marked the issue as primary issue
#1 - c4-sponsor
2023-01-19T21:47:06Z
livingrockrises marked the issue as sponsor confirmed
#2 - livingrockrises
2023-01-19T22:05:14Z
#486 is not duplicate of this primary issue.
#3 - livingrockrises
2023-01-19T22:10:44Z
#367 is not duplicate of this primary issue.
#4 - livingrockrises
2023-01-19T22:18:10Z
#233 is not related to this primary issue.
#5 - livingrockrises
2023-01-19T22:18:34Z
#175 is a different primary issue.
#6 - livingrockrises
2023-01-19T22:25:05Z
#168 is not duplicate of this primary issue.
#7 - gzeon-c4
2023-01-22T15:09:17Z
FYI you should look for the "MARKED DUPLICATES" section on the bottom, not sure why you quoting some issue I didn't mark as duplicate of this.
#8 - c4-judge
2023-02-10T12:34:37Z
gzeon-c4 marked the issue as satisfactory
#9 - c4-judge
2023-02-10T12:35:32Z
gzeon-c4 marked issue #36 as primary and marked this issue as a duplicate of 36
584.9482 USDC - $584.95
https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol#L68 https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/libs/MultiSend.sol#L26
The handleOps
function executes an array of UserOperation
. If at least one user operation fails the whole transaction will revert. That means the error on one user ops will fully reverts the other executed ops.
The multiSend
function reverts if at least one of the transactions fails, so it is also vulnerable to such type of attacks.
Relayer offchain verify the batch of UserOperation
s, convinced that they will receive fees, then send the handleOps
transaction to the mempool. An attacker front-run the relayers transaction with another handleOps
transaction that executes only one UserOperation
, the last user operation from the relayers handleOps
operations. An attacker will receive the funds for one UserOperation
. Original relayers transaction will consume gas for the execution of all except one, user ops, but reverts at the end.
Griefing attacks on the gas used for handleOps
and multiSend
function calls.
Please note, that while an attacker have no direct incentive to make such an attacks, they could short the token before the attack.
Remove redundant require
-like checks from internal functions called from the handleOps
function and add the non-atomic execution logic to the multiSend
function.
#0 - c4-judge
2023-01-18T00:23:27Z
gzeon-c4 marked the issue as duplicate of #90
#1 - c4-judge
2023-01-18T00:25:06Z
gzeon-c4 marked the issue as not a duplicate
#2 - c4-judge
2023-01-18T00:25:24Z
gzeon-c4 marked the issue as primary issue
#3 - livingrockrises
2023-02-07T09:17:24Z
once public will double check with infinitism community. marked acknowledged for now. and for multisend non-atomic does not make sense!
#4 - c4-sponsor
2023-02-07T09:17:31Z
livingrockrises marked the issue as sponsor acknowledged
#5 - c4-judge
2023-02-10T12:20:46Z
gzeon-c4 marked the issue as selected for report
666.6076 USDC - $666.61
Judge has assessed an item in Issue #504 as 2 risk. The relevant finding follows:
Incorrect signature check in the validatePaymasterUserOp function
#0 - gzeon-c4
2023-02-12T15:48:02Z
This is #504, was incorrectly downgraded to QA.
#1 - c4-judge
2023-02-12T15:48:18Z
gzeon-c4 marked the issue as duplicate of #466
#2 - c4-judge
2023-02-12T15:51:27Z
gzeon-c4 marked the issue as satisfactory
π Selected for report: peakbolt
Also found by: V_B, csanuragjain, zaskoh
449.9602 USDC - $449.96
https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol#L176 https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol#L265 https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol#L319 https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol#L363 https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol#L455 https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol#L458
According to the EIP-150 call
can consume as most 63/64
of parent calls' gas. That means that it is possible to manipulate the gas amount to be passed into calls mentioned in the "Links to affected code" section. Specifically, if the amount of gas that is left in the current execution flow is smaller than requiredGasForTheCall*64/63
(where requiredGasForTheCall
means the amount of gas that should be passed to the following call) then the wrong amount of gas will be passed, but the parent execution can be finished using the rest of the gas (which can be as big as requiredGasForTheCall/64
), so the refund for the transaction execution will be provided to the relayer.
Let's see the _executeUserOp
logic:
try this.innerHandleOp(userOp.callData, opInfo, context) returns (uint256 _actualGasCost) { collected = _actualGasCost; } catch { uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; collected = _handlePostOp(opIndex, IPaymaster.PostOpMode.postOpReverted, opInfo, context, actualGas); }
According to the 63/64 rule and code above, the innerHandleOp
could be reverted due to gas limit issue on the child call and the all remaining gas will be spent on the _handlePostOp
, that save the relayer refund. Thus, the relayer can fail execute transactions but invalidate the nonce and take funds for relay.
For a rough estimate of the cost of user operations that are subject to this type of attack, consider operation with 2_000_000 gas limit. Then, the 2_000_000 / 64 == 31_250, that should be enough to execute _handlePostOp
without paymaster and rewrite one storage slot.
Any sufficiently expensive transaction can be forcfully failed by the relayer, but the relayer will receive refund. The problem may be occurs on the malicious relayer or even honest if it will send relay transaction with certain gas.
Include logic related to the EIP-150 specification to the gas management. You can use the following:
require(gasleft()*63/64 >= requiredGasForTheCall);
contractAddress.call{gas: requiredGasForTheCall}(dataForTheCall);
#0 - c4-judge
2023-01-18T00:21:15Z
gzeon-c4 marked the issue as duplicate of #303
#1 - c4-sponsor
2023-02-09T11:09:53Z
livingrockrises marked the issue as sponsor acknowledged
#2 - c4-judge
2023-02-10T12:16:35Z
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
The SmartAccount
contract inherits many contracts, some of which have their own storage management logic. In case of an upgrade, adding new storage variables to the inherited contracts will colapse the storage layout. This will create an inconvenience for updates with possible vulnerabilities on storage overlaps.
Deterioration of code architecture in case of an upgrade that creates storage overlaps.
Use a base contract for all storage management logic related to the SmartAccount
. Another way to solve this is to add a storage gap to all contracts that SmartAccount
inherits.
#0 - c4-judge
2023-01-17T15:52:40Z
gzeon-c4 marked the issue as duplicate of #352
#1 - c4-sponsor
2023-02-09T11:33:45Z
livingrockrises marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-10T12:36:40Z
gzeon-c4 marked the issue as satisfactory