xanoutas/solidity-security-findings huggingface dataset:
Incorrect management of requested gas amount in EIP-4337 logic
Audit: 2023-01-biconomy-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Description

According to the [EIP-150](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md) `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.

## Attack scenario

Let's see the `_executeUserOp` logic: 

```solidity=
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.

## Impact

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. 

## Recommended Mitigation Steps

Include logic related to the [EIP-150](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md) specification to the gas
handleAggregatedOps() does not handle non-atomic transactions which results in whole function revert if one transaction does not go through
Audit: 2023-01-biconomy-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

Function reverts if one account or paymaster is not validated, which leads to a waste of time and gas.

## Proof of Concept

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
Users can accidentally lock their stakes forever
Audit: 2023-01-biconomy-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
StakeManager accepts user deposits and stakes. When adding a new stake, it allows the arbitrary value of ```_unstakeDelaySec``` supposedly it is higher than previous ```info.unstakeDelaySec```: 
```solidity:
    function addStake(uint32 _unstakeDelaySec) public payable {
        DepositInfo storage info = deposits[msg.sender];
        require(_unstakeDelaySec > 0, "must specify unstake delay");
        require(_unstakeDelaySec >= info.unstakeDelaySec, "cannot decrease unstake time");
        ...
    }
```
If the users want to unlock their stakes, they can only withdraw when the ```unstakeDelaySec``` has passed:
```solidity:
    function unlockStake() external {
        ...
        uint64 withdrawTime = uint64(block.timestamp) + info.unstakeDelaySec;
        info.withdrawTime = withdrawTime;
        ...
    }
```
There is no upper boundary for the ```_unstakeDelaySec``` value. If users accidentally set a too big value for ```_unstakeDelaySec```, their stake will be locked for life. This could happen if, for instance, a value of timestamp, not seconds are passed as a parameter. There should be a reasonable upper boundary for this value.

## Recommended Mitigation Steps
The protocol should prevent users from making such mistakes. Introduce a reasonable upper limit for ```_unstakeDelaySec```, e.g. 5 years.
Unpreparedness for upgrades contracts that `SmartAccount` inherits
Audit: 2023-01-biconomy-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Description

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.

## Impact

Deterioration of code architecture in case of an upgrade that creates storage overlaps.

## Recommended Mitigation Steps

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.
Griefing attacks on `handleOps` and `multiSend` logic
Audit: 2023-01-biconomy-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Description

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.

## Attack scenario

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.

## Impact

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. 

## Recommended Mitigation Steps

Remove redundant `require`-like checks from internal functions called from the `handleOps` function and add the non-atomic execution logic to the `multiSend` function.
[Medium-3] Non-compliance with EIP-4337
Audit: 2023-01-biconomy-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

Some parts of the codebase are not compliant with the EIP-4337 from the [EIP-4337 specifications](https://eips.ethereum.org/EIPS/eip-4337#specification), at multiple degrees of severity.

## Proof of Concept

### Sender existence

```text
Create the account if it does not yet exist, using the initcode provided in the UserOperation. If the account does not exist, and the initcode is empty, or does not deploy a contract at the “sender” address, the call must fail.
```

If we take a look at the [`_createSenderIfNeeded()`]() function, we can see that it's not properly implemented:
```solidity
function _createSenderIfNeeded(uint256 opIndex, UserOpInfo memory opInfo, bytes calldata initCode) internal {
	if (initCode.length != 0) {
		address sender = opInfo.mUserOp.sender;
    	if (sender.code.length != 0) revert FailedOp(opIndex, address(0), "AA10 sender already constructed");
      	address sender1 = senderCreator.createSender{gas: opInfo.mUserOp.verificationGasLimit}(initCode);
        if (sender1 == address(0)) revert FailedOp(opIndex, address(0), "AA13 initCode failed or OOG");
        if (sender1 != sender) revert FailedOp(opIndex, address(0), "AA14 initCode must return sender");
        if (sender1.code.length == 0) revert FailedOp(opIndex, address(0), "AA15 initCode must create sender");
        address factory = address(bytes20(initCode[0:20]));
      	emit AccountDeployed(opInfo.userOpHash, sender, factory, opInfo.mUserOp.paymaster);
	}
}
```

The statement in the EIP implies that if the account does not exist, the initcode **must** be used.
In this case, it first check if the initcode exists, but this condition should be checked later.

This could be rewritten to:

```solidity
function _createSenderIfNeeded(uint256 opIndex, UserOpInfo memory opInfo, bytes calldata initCode) internal {
	address sender = opInfo.mUserOp.sender;
	if (sender.code.length == 0) {
		require(initCode.length != 0, "empty initcode");
		address sender1 = senderCreator.createSender{gas: opInfo.mUserOp.verificationGasLimit}(initCode);
        if (sender1 == address(0)) revert FailedOp(opIndex, address(0), "AA13 initCode failed or OOG");
        if (sender1 != sender) revert FailedOp(opIndex, address(0), "AA14 initCode must return sender");
        if (sender1.code.length == 0) revert FailedOp(opIndex, address(0), "AA15 initCode must create sender");
        address factory = address(bytes20(initCode[0:20]));
      	emit AccountDeployed(opInfo.userOpHash, sender, factory, opInfo.mUserOp.paymaster);
	}
}
```

### Account

T
Calling execute() and executeBatch() functions in SmartAccount.sol from the EntryPoint will fail
Audit: 2023-01-biconomy-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
The function `_requireFromEntryPointOrOwner()` is being called within the `execute()` and `executeBatch()` functions to check if the `msg.sender` is either the owner or the `EntryPoint` contract, but these functions have `onlyOwner()` modifier, which will only allow the owner to execute these functions. 

    function execute(address dest, uint value, bytes calldata func) external onlyOwner {
        _requireFromEntryPointOrOwner();
        _call(dest, value, func);
    }

    function executeBatch(address[] calldata dest, bytes[] calldata func) external onlyOwner{
        _requireFromEntryPointOrOwner();
        require(dest.length == func.length, "wrong array lengths");
        for (uint i = 0; i < dest.length;) {
            _call(dest[i], 0, func[i]);
            unchecked {
                ++i;
            }
        }
    }

## Proof of Concept
Calling these functions from the `EntryPoint` contract will fail

## Tools Used
VSCode

## Recommended Mitigation Steps
Remove `onlyOwner` modifier from both functions
[Medium-2] Sponsored transactions can be abused for profit
Audit: 2023-01-biconomy-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

When a user executes a transaction with their [`SmartAccount`](https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol), the function [`execTransaction()`](https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L192) can be used.
In some cases, some protocols want to get promoted and will sponsorize users of the abstract wallet transactions by refunding them back their gas usage in their protocol tokens.

This function starts by calculating a `startGas`. To do so, it starts by making an estimate of the price of the transaction with the "base gas" (21000), and then add the calldata cost.

From the 27th page of the [Ethereum yellow paper](https://ethereum.github.io/yellowpaper/paper.pdf), The cost for 0 byte values in the calldata is 4 units of gas, and the one for non-zero bytes is 16:
![e44e2a116d23afbc608d2309832dac52.png](:/d58e5bc939fe43f1982f164f36e97724)

In the current implementation, an estimate of 8 was done:

```solidity
// initial gas = 21k + non_zero_bytes * 16 + zero_bytes * 4
//            ~= 21k + calldata.length * [1/3 * 16 + 2/3 * 4]
uint256 startGas = gasleft() + 21000 + msg.data.length * 8;
```

This appears to be wrong because there is no way to predict how much 0 bytes will a user input as the calldata of this function, which is the same for non-zero bytes in the calldata.

This means that sending a transaction with a calldata that is filled with 0 is going to cause `startGas` to overestimate the price of the calldata, and is going to send too much tokens from the sponsor.

Sponsors probably want their tokens distributed in a fair way, and not being abused by malicious users crafting calldata transaction filled with 0.

## Proof of Concept

See the following gist which a test case for abusing of this: https://gist.github.com/iFrostizz/b075edb97b39eaac333315ee927f5d14
Please note that due to foundry limitations (because of the gas limit, while creating the calldata filled with 0), I could **not** clearly show how much tokens could be stolen. But the trick consists of filling the `Transaction` `data` field with a maximum of 0's.

## Tools Used

Manual inspection

## Recommended Mitigation Steps

Make a calculation on the lower end, by assuming that the calldata is only filled with 0 (even when not) to avoid giving away too much tokens from the sponsor.

```solidity
function execTransaction(
	Transaction memory _tx,
    uint256 batchId,
    FeeRefund memory refundInfo,
    bytes memory signatures
) public payable virtual override returns (bool success) {
		uint256 startGas = gasleft() + 21000 + msg.data.length * 4;
		/* Snip */
}
```
Loss of funds if mistakenly sent to the implementation contract 
Audit: 2023-01-biconomy-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

The `SmartAccount` contract that is used as the implementation contract behind the Proxy defines the `receive` function.

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

```solidity
receive() external payable {}
```

This will allow the implementation contract to receive ETH. 

## Impact

A user may mistakenly send funds to the implementation contract. These transactions will succeed, as the contract implements the `receive() payable` function.

Any ETH sent to the implementation contract will be lost, as this contract represents the implementation logic and not the proper user's wallet (the proxy).

## PoC

The following test illustrates the issue.

```solidity
contract AuditTest is Test {
    bytes32 internal constant ACCOUNT_TX_TYPEHASH = 0xc2595443c361a1f264c73470b9410fd67ac953ebd1a3ae63a2f514f3f014cf07;

    uint256 bobPrivateKey = 0x123;
    uint256 attackerPrivateKey = 0x456;

    address deployer;
    address bob;
    address attacker;
    address entrypoint;
    address handler;

    SmartAccount public implementation;
    SmartAccountFactory public factory;
    MockToken public token;

    function setUp() public {
        deployer = makeAddr("deployer");
        bob = vm.addr(bobPrivateKey);
        attacker = vm.addr(attackerPrivateKey);
        entrypoint = makeAddr("entrypoint");
        handler = makeAddr("handler");

        vm.label(deployer, "deployer");
        vm.label(bob, "bob");
        vm.label(attacker, "attacker");

        vm.startPrank(deployer);
        implementation = new SmartAccount();
        factory = new SmartAccountFactory(address(implementation));
        token = new MockToken();
        vm.stopPrank();
    }
    
    function test_SmartAccount_SendEthToImplementation() public {
        address proxy = factory.deployWallet(bob, entrypoint, handler);
        SmartAccount wallet = SmartAccount(payable(proxy));

        uint256 amount = 1 ether;
        (bool sent,) = address(implementation).call{value: 1 ether}("");
        require(sent);

        assertEq(address(implementation).balance, amount);
        assertEq(address(wallet).balance, 0);
    }
}
```

## Recommendation

Remove  the `receive` function from the `SmartAccount` contract. The `receive` function should only be implemented in the `Proxy` contract.
THE ETHEREUM WILL GET LOCKED IN THE CONTRACT IF THE EXTERNAL CALL FUNCTION REVERTS DUE TO REQUIRE FUNCTION FAILURE
Audit: 2023-01-biconomy-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

The Ethereum could get locked inside the `BasePayMaster.sol` contract if the external call to the `EntryPoint` contract reverts due to require statement failure. In such a scenario there is no withdraw function inside the `BasePayMaster.sol` contract to withdraw the Eth.

## Proof of Concept

The `addStake` is an external payable function defined in the `BasePayMaster.sol` contract. If an external function calls this function with `msg.value` then the `msg.value` is sent to the EntryPoint.sol contract since `entryPoint.addStake` function is called with the input variable `unstakeDelaySec`. 

    function addStake(uint32 unstakeDelaySec) external payable onlyOwner {
        entryPoint.addStake{value : msg.value}(unstakeDelaySec);
    }

But inside the `addStake` function in the StakeManager.sol contract there is require function to check whether `unstakeDelaySec > 0` and `_unstakeDelaySec >= info.unstakeDelaySec`. 

    function addStake(uint32 _unstakeDelaySec) public payable {
        DepositInfo storage info = deposits[msg.sender];
        require(_unstakeDelaySec > 0, "must specify unstake delay");
        require(_unstakeDelaySec >= info.unstakeDelaySec, "cannot decrease unstake time");

If any of the above require statement fail then the transaction will revert and the ethereum will be sent back to the `BasePayMaster.sol` and not to the original `msg.sender`. But there is no withdraw function inside the `BasePayMaster.sol` contract to withdraw the Ether and hence the Ether will be locked inside the `BasePayMaster.sol` contract.

## Tools Used
Manual Review, VSCode

## Recommended Mitigation Steps
Return a Boolean success value to the `addStake` function of the `BasePayMaster.sol` contract based on result of the `addStake` external call function execution of the `StakeManager` contract. 

If the external call transaction is a failure the `addStake` function of the `BasePayMaster.sol` can revert the transaction and can transfer back the ether to the `msg.sender`. Hence the ether will not be locked inside the `BasePayMaster.sol` contract.