xanoutas/solidity-security-findings huggingface dataset:
Fee on transfer tokens in the protocol can lead to incorrect transfer
Audit: 2024-03-coinbase-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/MagicSpend/MagicSpend.sol#L334


# Vulnerability details

## Impact
Fee on transfer tokens transfer less tokens in than what would be expect. This means that the protocol request incorrect amounts when dealing with these tokens.

## Proof of Concept
Some fee on transfer tokens, do not transfer the full amount, but subtracts it from sending balance to the  sender. Some tokens prefer this approach, to make the amount received by the recipient an exact amount. Therefore, after funds are send to users, balance becomes less than it should be. So this contract does not fully support fee on transfer tokens. With such tokens, user funds can get lost after transfers.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Check the balance before and after transfer


## Assessed type

ERC20
_validateUserOp reverts before validating User signature
Audit: 2024-03-coinbase-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-coinbase/blob/main/src/SmartWallet/CoinbaseSmartWallet.sol#L122-L168


# Vulnerability details

## Impact
The validateUserOp function is a custom implementation of the ERC-4337 validateUserOp method. It is called by an EntryPoint contract to validate a user operation before executing it. The validation ensures that the operation meets certain criteria before proceeding, Which includes confirming if key is REPLAYABLE_NONCE_KEY or not. If the key is not identified to be a REPLAYABLE_NONCE_KEY, the function reverts.
The issue lies in the complementing if/else state which also reverts if the key is identified to be a REPLAYABLE_NONCE_KEY.
Since either of this conditions is always going to be true, the function fails to proceed in validating the user 

## Proof of Concept
A. first part of the function where the key is derived from userOp calldata
```solidity
    function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds)
        public
        payable
        virtual
        onlyEntryPoint
        payPrefund(missingAccountFunds)
        returns (uint256 validationData)
    {
        uint256 key = userOp.nonce >> 64;
```
B. If/else statement assessing if the key is REPLACEABLE_NONCE_KEY or not. 
Both statements reverts regardless of the status of the key
```solidity
 if (key != REPLAYABLE_NONCE_KEY) {
                revert InvalidNonceKey(key);
            }
        } else {
            if (key == REPLAYABLE_NONCE_KEY) {
                revert InvalidNonceKey(key);
            }
```
C. The very last part of the function for validating signature is skipped
```solidity
 // Return 0 if the recovered address matches the owner.
        if (_validateSignature(userOpHash, userOp.signature)) {
            return 0;
        }
```

## Tools Used
Manual review, vs code

## Recommended Mitigation Steps
The revert statement after checking if key is REPLAYABLE_NONCE_KEY should be removed to allow the the remaining part of the code to be executed.
i. this else statement should be removed 
``` solidity
else {
            if (key == REPLAYABLE_NONCE_KEY) {
                revert InvalidNonceKey(key);
            }
```


## Assessed type

Error
owner of MagicSpend can remove confirmed sponsored ETH amount from users 
Audit: 2024-03-coinbase-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/MagicSpend/MagicSpend.sol#L203


# Vulnerability details

## Impact

Users that will use `MagicSpend` contract as their paymaster can have their assigned/sponsored ETH amount removed by the MagicSpend contract owner.

Even if they had their sponsored amount confirmed by the `MagicSpend#validatePaymasterUserOp` function returning a context, they can lose their assigned ETH if the MagicSpend owner takes out ETH from the contract itself.


## Proof of Concept

User can have their sponsored amount on validation trough the call from the `EntryPont` to `MagicSpend#validatePaymasterUserOp` marking an amount as withdrawable ETH for the user account :

https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/MagicSpend/MagicSpend.sol#L138

The user can then use the supplied withdraw function to get its funds out of the contract :

https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/MagicSpend/MagicSpend.sol#L169
https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/MagicSpend/MagicSpend.sol#L181


The problem is in the function `ownerWithdraw` that is only callable by the MagicSpend contract owner that can remove ETH amount that is marked as withdrawable by a user account in the previous `validatePaymasterUserOp` call.

https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/MagicSpend/MagicSpend.sol#L203

## Tools Used
Manual review

## Recommended Mitigation Steps

Don't allow the MagicSpend owner to take out confirmed user ETH amount :

```diff
    function min(uint256 a, uint256 b) internal pure returns (uint256) {
        return a < b ? a : b;
    }

    function validatePaymasterUserOp(UserOperation calldata userOp, bytes32, uint256 maxCost)
        external
        onlyEntryPoint
        returns (bytes memory context, uint256 validationData)
    {
        _withdrawableETH[userOp.sender] += withdrawAmount - maxCost;
+       ethOwnedToUsers += withdrawAmount - maxCost;  
        context = abi.encode(maxCost, userOp.sender);
    }

    function ownerWithdraw(address asset, address to, uint256 amount) external onlyOwner {
+       if (asset == address(0) && ethOwnedToUsers > 0) {
+              uint256 availableBalance = this.balance - ethOwnedToUsers;
+              amount = min(amount, availableBalance); // limit the amount of ETH that can be withdrawn by the owner
+       }
        _withdraw(asset, to, amount);
    }
```
Don't forget to decrease the `ethOwnedToUsers` variable when the amount is withdrawn to the user !





## Assessed type

Error
wrong check for ETH balance is done inside the MagicSpend#validatePaymasterUserOp which can lead to reverts inside MagicSpend#postOp
Audit: 2024-03-coinbase-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/MagicSpend/MagicSpend.sol#L133


# Vulnerability details

## Impact

If the MagicSpend paymaster is called by the EntryPoint with an aggregation of user/s operations the check done inside `MagicSpend#validatePaymasterUserOp` for the ETH balance is not enough and can produce a revert when `MagicSpend@postOp` is executed putting this paymaster on a [bad reputation](https://eips.ethereum.org/EIPS/eip-4337#reputation-rationale) by wrongly pass the validation and working only with a bundle of size 1 for the user operation.

If a MagicSpend paymaster contract is used with `handleAggregatedOps` we get the same revert.

## Proof of Concept

The revert happen becouse 'MagicSpend#validatePaymasterUserOp' and `MagicSpend#postOp` are coded to be called **sequentially** one after the other. But in the `EntryPoint` contract both function are called separately in a for loop which hinders the 'MagicSpend#validatePaymasterUserOp' ETH balance validation on this line : 

https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/MagicSpend/MagicSpend.sol#L133

The validation **passes** and when looping on `postOp` function the revert can happen on this line becouse the function `SafeTransferLib.forceSafeTransferETH` checks the balance and if its less then amount to send it reverts with `ETHTransferFailed()`

https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/MagicSpend/MagicSpend.sol#L160-L162

```solidity
    function forceSafeTransferETH(address to, uint256 amount, uint256 gasStipend) internal {
        /// @solidity memory-safe-assembly
        assembly {
            if lt(selfbalance(), amount) {
                mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`. @<- here correctly reverts
                revert(0x1c, 0x04)
            }
        ...
```

The following POC is a new test inside the `Integration.t.sol` file which demonstrates that if we construct an array of UserOps operations that is bigger then one the test will fail with a revert of `ETHTransferFailed()` when executing `MagicSpend::postOp`. 

```solidity
    function test_MoreThanOneUserOpsPaymasterPays() public {
        uint256 NUMBER_OF_USER_OPS = 2; // @-> to see test succeed change this to 1

        UserOperation[] memory ops = new UserOperation[](NUMBER_OF_USER_OPS);

        for (uint256 i; i<NUMBER_OF_USER_OPS; i++) {
            nonce = i; // change nonce for paymasterAndData field generation !

            UserOperation memory op = _getUserOp();
            op.nonce = i; // change use nonce

            bytes32 hash = entryPoint.getUserOpHash(op);
            console.logBytes32(hash);
            (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountOwnerPk, hash);
            bytes memory userOpSig =
                abi.encode(CoinbaseSmartWallet.SignatureWrapper({ownerIndex: 0, signatureData:
An Attacker can Prevent  executeBatch  function from working as intended (DOS)
Audit: 2024-03-coinbase-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/SmartWallet/CoinbaseSmartWallet.sol#L200-L212
https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/SmartWallet/CoinbaseSmartWallet.sol#L264-L277


# Vulnerability details

## Impact
When a transaction encounters an out-of-gas error and reverts without providing a specific revert reason, the Ethereum Virtual Machine (EVM) will revert the entire transaction and discard any state changes made up to that point. Additionally, when a transaction contains multiple external calls (such as an array of calls), and one of these calls encounters an out-of-gas error and reverts, the entire transaction will revert. Furthermore, the transaction will stop executing further calls in the array once the error occurs. This behavior ensures that transactions are atomic and either all operations within a transaction are executed successfully, or none of them are.

## Proof of Concept
A malicious user can exploit this behavior by setting up a target address to use more gas since there is no specified gas limit for each call. When the contract provide a list of calls, along with their associated values and data, to be executed in batches. If one of these calls encounters an out-of-gas error, it will trigger a revertion of the entire function. This issue persists if a gas limit is not set for each call.


## Tools Used
Manual code analysis

## Recommended Mitigation Steps
To mitigate the risk of out-of-gas errors and ensure proper handling of transactions containing multiple external calls, consider the following steps:

1. **Set Gas Limits**: Specify a gas limit for each external call to ensure that there's sufficient gas allocated for its execution. This prevents out-of-gas errors and ensures that transactions can be executed within the specified gas limits.




By implementing these mitigation steps, you can reduce the risk of out-of-gas errors and ensure the robustness and reliability of your smart contracts.





## Assessed type

call/delegatecall
userOp struct nonce isn't validated this will impact functionalities that rely on uniqueness as nonce isn't updated
Audit: 2024-03-coinbase-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-coinbase/blob/main/src%2FSmartWallet%2FCoinbaseSmartWallet.sol#L137-L162


# Vulnerability details

If `userOp.callData[0:4]`is equal to function  selector `0xbf6ba1fc`(`executeWithoutChainIdValidation(bytes))`, it will check if `key` is not equal to `REPLAYABLE_NONCE_KEY` which is hardcoded as `8453` to enforce sequential sequencing of replayable transactions, and if so, it will revert with an "InvalidNonceKey" error. 
If key is equal to `REPLAYABLE_NONCE_KEY`, it will also revert with the same error.
If the recovered address matches the owner's signature, it will return 0.
Otherwise, it will return 1, which is determined by the bitwise operations and conditions specified in the comment.

```solidity
    function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds)
        public
        payable
        virtual
        onlyEntryPoint
        payPrefund(missingAccountFunds)
        returns (uint256 validationData)
    {
        uint256 key = userOp.nonce >> 64;

        // 0xbf6ba1fc = bytes4(keccak256("executeWithoutChainIdValidation(bytes)"))
        if (userOp.callData.length >= 4 && bytes4(userOp.callData[0:4]) == 0xbf6ba1fc) {
            userOpHash = getUserOpHashWithoutChainId(userOp);
            if (key != REPLAYABLE_NONCE_KEY) {
                revert InvalidNonceKey(key);
            }
        } else {
            if (key == REPLAYABLE_NONCE_KEY) {
                revert InvalidNonceKey(key);
            }
        }

        // Return 0 if the recovered address matches the owner.
        if (_validateSignature(userOpHash, userOp.signature)) {
            return 0;
        }

        // Else return 1, which is equivalent to:
        // `(uint256(validAfter) << (160 + 48)) | (uint256(validUntil) << 160) | (success ? 0 : 1)`
        // where `validUntil` is 0 (indefinite) and `validAfter` is 0.
        return 1;
    }
```
The issue is the nonce isn't validated and increased after the `userop` is validated if the `REPLAYABLE_NONCE_KEY` is used for replayable transactions, it should ideally be incremented by one for each new transaction to enforce sequential sequencing and prevent replay attacks.

In any case where  the condition `key != REPLAYABLE_NONCE_KEY` is met, it should increase the nonce to enforce sequential sequencing of replayable transactions.

If a transaction is not using the `REPLAYABLE_NONCE_KEY` and the condition `key == REPLAYABLE_NONCE_KEY` is met, it should also increase the nonce to maintain sequential order.

These conditions should trigger an increase in the nonce to ensure that each transaction with the replayable nonce key is unique and correctly sequenced.
## Impact
Without the increase in nonce during the validation stale nonces can be used impacting some functionality that rely on the uniqueness of the operation.
## Proof of Concept
 

## Tools Used
Manual Review

## Recommended Mitigation Steps
Recommended to include
Lack of proper error handling in Magicspend to handle failures of an external calls.
Audit: 2024-03-coinbase-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/MagicSpend/MagicSpend.sol#L226-L234


# Vulnerability details

## Impact

The lack of proper error handling for a call made to an external contract exposes the smart contract to various risks and vulnerabilities. Without adequate error handling, failed external calls can lead to unexpected behavior, including  state inconsistencies and potential security breaches.

## Proof of Concept

The lack of proper error handling for external calls can be illustrated with the following code snippet:

```solidity
 /// @notice Adds stake to the EntryPoint.
    ///
    /// @dev Reverts if not called by the owner of the contract.
    ///
    /// @param amount              The amount to stake in the Entrypoint.
    /// @param unstakeDelaySeconds The duration for which the stake cannot be withdrawn.
    function entryPointAddStake(uint256 amount, uint32 unstakeDelaySeconds) external payable onlyOwner {
        IEntryPoint(entryPoint()).addStake{value: amount}(unstakeDelaySeconds);
    }
```

In this code, the function `entryPointAddStake` makes an external call to `IEntryPoint(entryPoint())` without checking the return value for success or failure. If the external call fails (e.g., due to out-of-gas, invalid inputs, or contract reverts), the transaction will continue executing, potentially leading to unintended consequences.

## Tools Used

- Manual code analysis

## Recommended Mitigation Steps
To mitigate the risks associated with the lack of proper error handling for external calls, the following steps are recommended:

1. **Implement Error Handling**: Ensure that all external calls include proper error handling mechanisms to handle potential failures. Check the return value of external calls and revert the transaction if the call fails unexpectedly.

```solidity
/// @notice This function adds stake to the EntryPoint contract.
///
/// @dev It reverts if not called by the owner of the contract.
///
/// @param amount The amount of ether to stake in the EntryPoint.
/// @param unstakeDelaySeconds The duration for which the stake cannot be withdrawn.
function entryPointAddStake(uint256 amount, uint32 unstakeDelaySeconds) external payable onlyOwner {
    // Use `call` to add ether directly to the entry point address.
    // However, the function neglects to handle the call in case it fails.
    // This has been identified through manual code analysis.

    // Mitigation: Consider creating a bool variable that reverts if the transaction fails.
    bool success;
    (success, ) = address(entryPoint()).call{value: amount}(abi.encodeWithSignature("addStake(uint32)", unstakeDelaySeconds));
    require(success, "Add stake call failed");
}
```


2. **Limit External Call Gas**: Use the `gas` parameter when making external calls to limit the amount of gas forwarded to the callee. This helps prevent potential out-of-gas errors and ensures that the transaction can
`initialize` function will fail to set the owners if  OwnerIndex is already populated by the constructor at deployment
Audit: 2024-03-coinbase-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-coinbase/blob/main/src%2FSmartWallet%2FCoinbaseSmartWallet.sol#L102-L107
https://github.com/code-423n4/2024-03-coinbase/blob/main/src%2FSmartWallet%2FCoinbaseSmartWallet.sol#L114-L120


# Vulnerability details

The wallet  account contains a constructor that sets the initial owners using the following code:
```solidity
constructor() {
    bytes[] memory owners = new bytes[](1);
    owners[0] = abi.encode(address(0));
    _initializeOwners(owners);
}
```
Additionally, there is an `initialize` function that sets owners after deployment:

```solidity
function initialize(bytes[] calldata owners) public payable virtual {
    if (nextOwnerIndex() != 0) {
        revert Initialized();
    }
    _initializeOwners(owners);
}
```
The `nextOwnerIndex` function is used to check if owners are already set, and it returns the next owner index from storage.

## Impact
The` initialize` function checks if owners are already set using `nextOwnerIndex() != 0 `and reverts if this condition is true. This means that if the `constructor` has already set owners, the `initialize` function will revert when attempting to set owners again. Thi will limit the flexibility of the contract, as it prevents the `initialize` function from setting additional owners if they were already set in the constructor.

## Proof of Concept


## Tools Used
Manual Review 
## Recommended Mitigation Steps
Recommendation Consider revising the` initialize` function to allow setting owners regardless of whether they were already set in the `constructor` but revert if `owner` already exists in the index. This will provide more flexibility in ownership management post-deployment.


## Assessed type

Context
Lack of signature deadline means old cross-chain replayable signatures could be forcibly replayed long after the user expected
Audit: 2024-03-coinbase-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-coinbase/blob/main/src/SmartWallet/CoinbaseSmartWallet.sol#L180


# Vulnerability details

**Impact**

It is likely that Coinbase will wish to make their wallet available to customers on different EVM chains in the future if there is sufficient user demand for it. However should they integrate on a new chain, all of the account owner's previously signed `executeWithoutChainIdValidation` signatures would remain valid and could be replayed by any malicious actor whenever the current replayable nonce of that account matches that of the previously signed transaction. This could lead to scenarios where addresses/keys that were once added as owners but then later removed are able to gain access to this account again on the newly integrated chain.

**Proof Of Concept**
Consider the following scenario:
1. Alice has a CoinbaseSmartWallet at launch and uses it normally on the EVM chains it is deployed to.
2. Alice adds a second owner via `executeWithoutChainIdValidation`.
3. Some time passes and Alice decides to remove the second owner.
4. Coinbase adds support for another EVM chain (Fantom, Blast, an L2 launched in the future, etc)
5. If Alice starts using this chain without making a transaction to consume the first nonce that used the `REPLAYABLE_NONCE_KEY` someone would be able to add the previously added the second owner on this chain by replaying the old signature.

**Recommended Mitigation**
The protocol should consider adding a `deadline` parameter to it's `SignatureWrapper` struct which can be checked in `_validateSignature`, this would remove the need to be concerned about stale signatures being reused far into the future.




## Assessed type

Other