xanoutas/solidity-security-findings huggingface dataset:
Balance check during MagicSpend validation cannot ensure that MagicSpend has enough balance to cover the requested fund.
Audit: 2024-03-coinbase-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
[Balance check](https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/MagicSpend/MagicSpend.sol#L130-L135) during MagicSpend validation cannot ensure that MagicSpend has enough balance to cover the requested fund.

```solidity
        // Ensure at validation that the contract has enough balance to cover the requested funds.
        // NOTE: This check is necessary to enforce that the contract will be able to transfer the remaining funds
        //       when `postOp()` is called back after the `UserOperation` has been executed.
        if (address(this).balance < withdrawAmount) {
            revert InsufficientBalance(withdrawAmount, address(this).balance);
        }
```

## Proof of Concept
1. EntryPoint executes userOps by two loops: [validation loop](https://github.com/eth-infinitism/account-abstraction/blob/abff2aca61a8f0934e533d0d352978055fddbd96/contracts/core/EntryPoint.sol#L98-L102) and [execution loop](https://github.com/eth-infinitism/account-abstraction/blob/abff2aca61a8f0934e533d0d352978055fddbd96/contracts/core/EntryPoint.sol#L107-L109).
2. Suppose we has two userOps using `MagicSpend`
* userOp1 with `withdrawAmount` 100
* userOp2 with `withdrawAmount` 150
3. userOp1 and userOp2 are packed together as `UserOperation[]` [ops](https://github.com/eth-infinitism/account-abstraction/blob/abff2aca61a8f0934e533d0d352978055fddbd96/contracts/core/EntryPoint.sol#L92) and ops will be executed by entryPoint through [the `handleOps` method](https://github.com/eth-infinitism/account-abstraction/blob/abff2aca61a8f0934e533d0d352978055fddbd96/contracts/core/EntryPoint.sol#L92)
4. Suppose the balance of MagicSpend is 200.
5. During the validation loop
* `validatePaymasterUserOp` will be called during userOp1 validation and pass for  address(this).balance (=200) is larger than withdrawAmount (=100)
* `validatePaymasterUserOp` will be called during userOp1 validation and pass for  address(this).balance (=200) is larger than withdrawAmount (=150)
```solidity
        // https://github.com/eth-infinitism/account-abstraction/blob/abff2aca61a8f0934e533d0d352978055fddbd96/contracts/core/EntryPoint.sol#L98C1-L102C10
        for (uint256 i = 0; i < opslen; i++) {
            UserOpInfo memory opInfo = opInfos[i];
            (uint256 validationData, uint256 pmValidationData) = _validatePrepayment(i, ops[i], opInfo);
            _validateAccountAndPaymasterValidationData(i, validationData, pmValidationData, address(0));
        }
```
6. During the execution loop
* The balance of MagicSpend is 200
* The request fund of userOp1 and userOp2 is 100+150=250 > 200
```solidity
        // https://github.com/eth-infinitism/account-abstraction/blob/abff2aca61a8f0934e533d0d352978055fddbd96/contracts/core/EntryPoint.sol#L107-L109
        for (uint
Missing Input Validation in CoinbaseSmartWallet Contract
Audit: 2024-03-coinbase-findings
Severity: MEDIUM

# Lines of code

https://github.com/coinbase/smart-wallet/blob/2b35f1a2e3a559e378c29fdb2c1e60ea47bd5929/src/CoinbaseSmartWallet.sol#L114


# Vulnerability details

# Functions reference:

1. **initialize() Function**
   - [Direct Link to initialize()](https://github.com/coinbase/smart-wallet/blob/2b35f1a2e3a559e378c29fdb2c1e60ea47bd5929/src/CoinbaseSmartWallet.sol#L114)

2. **execute() Function**
   - [Direct Link to execute()](https://github.com/coinbase/smart-wallet/blob/2b35f1a2e3a559e378c29fdb2c1e60ea47bd5929/src/CoinbaseSmartWallet.sol#L196)

3. **executeBatch() Function**
   - [Direct Link to executeBatch()](https://github.com/coinbase/smart-wallet/blob/2b35f1a2e3a559e378c29fdb2c1e60ea47bd5929/src/CoinbaseSmartWallet.sol#L205)

# Description:

The CoinbaseSmartWallet contract has missing input validation checks in some functions, potentially allowing invalid inputs to be passed, leading to unexpected behavior or security vulnerabilities. The affected functions are `initialize()`, `execute()`, and `executeBatch()`.

## Steps to Reproduce:

1. Deploy the CoinbaseSmartWallet contract.
2. Call the `initialize()` function with an empty owners array.
3. Call the `execute()` function with an invalid target address (e.g., address(0)).
4. Call the `executeBatch()` function with an array of Call structs containing invalid target addresses or incorrect values.

Expected Behavior:

The contract should revert when attempting to:

* Initialize with an empty owners array.
* Execute a call with an invalid target address.
* Execute a batch of calls with invalid target addresses or incorrect values.

Actual Behavior:

The contract does not revert and allows invalid inputs to be passed, potentially leading to unexpected behavior or security vulnerabilities.

# Proof of Concept (POC):

Here's a simple test contract to demonstrate the missing input validation issues:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./CoinbaseSmartWallet.sol";

contract CoinbaseSmartWalletTest is CoinbaseSmartWallet {
    function testInitializeEmptyOwnersArray() public {
        bytes[] memory emptyOwnersArray = new bytes[](0);
        initialize(emptyOwnersArray); // Should revert, but does not
    }

    function testExecuteInvalidTargetAddress() public payable {
        address invalidTarget = address(0);
        uint256 value = 1 ether;
        bytes memory data = abi.encodeWithSelector(this.test.selector);

        execute(invalidTarget, value, data); // Should revert, but does not
    }

    function testExecuteBatchInvalidTargetAddresses() public payable {
        Call[] memory invalidCalls = new Call[](1);
        invalidCalls[0] = Call({
            target: address(0),
            value: 1 ether,
            data: abi.encodeWithSelector(this.test.selector)
        });

        executeBatch(invalidCalls); // Should revert, but does not
    }

    function test() external pure {}
}
```
## Recommendation:

Add input validation checks to the `in
Cross-Chain Signature Replay Attack
Audit: 2024-03-coinbase-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
User operations can be replayed on smart accounts accross different chains. This can lead to user's losing funds or any unexpected behaviour that transaction replay attacks usually lead to  a DOs attack

## Proof of Concept
As specified by the EIP4337 standard to prevent replay attacks ... the signature should depend on chainid. UserOperation can be replayed on a different chain for the same smart contract account if the verifyingSigner is the same (and most likely this will be the case). The encode is not complex enough
## Tools Used

## Recommended Mitigation Steps
1. can add UserOperation calldata userOp parameter to get access to callGasLimit , verificationGasLimit and maxPriorityFeePerGas

2.Add this code in the calculation  
```diff
    function getHash(address account, WithdrawRequest memory withdrawRequest)
  getHash(address account, WithdrawRequest memory withdrawRequest) public view returns (bytes32) {  
        return keccak256(abi.encode(
                		 
                address(this),
                account,
                block.chainid,
                withdrawRequest.asset,
                withdrawRequest.amount,
                withdrawRequest.nonce,
                withdrawRequest.expiry,
+               msg.Sender
            ));
    }
```




## Assessed type

Error
Smart Contract Wallet Fails to Support Transfer of Funds During Execution Only
Audit: 2024-03-coinbase-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-coinbase/blob/main/src/MagicSpend/MagicSpend.sol#L109
https://github.com/code-423n4/2024-03-coinbase/blob/main/src/MagicSpend/MagicSpend.sol#L181
https://github.com/code-423n4/2024-03-coinbase/blob/main/src/MagicSpend/MagicSpend.sol#L315


# Vulnerability details

## Impact
ERC-4337 compliant Smart Contract wallets do not support Transfer funds during execution only. Specifically calling `withdraw()` in execution will revert due redundant `_validateRequest()` checks. 

## Proof of Concept
When the withdrawing account is an ERC-4337 compliant smart contract (like Smart Wallet), there are three different ways the Magic Spend smart contract can be used:
1. Pay gas only
2. Transfer funds during execution only
3. Pay gas and transfer funds during execution

Users have the capability to utilize their assets from CoinBase.com as if they were on-chain, enabling them to pay for gas and withdraw funds for any arbitrary operation.

This report focuses specifically on the scenario "Transfer funds during execution only" with `withdraw()` passed in as calldata:


1. The user submits an ERC-4337 UserOperation to the bundler.
2. The bundler simulates the validation of the UserOperation to ensure it can pay for its own execution and bundles it into a transaction. 
3. The EntryPoint calls `validateUserOp()` with SCW, then decrements PayMaster deposits.
4. EntryPoint invokes `validatePaymasterUserOp()` on MagicSpend/PayMaster. In this function, `_validateRequest()` is triggered, flipping the corresponding `_nonceUsed` mapping to `true`.

```solidity
    function validatePaymasterUserOp(UserOperation calldata userOp, bytes32, uint256 maxCost)
        external
        onlyEntryPoint
        returns (bytes memory context, uint256 validationData)
    {
        WithdrawRequest memory withdrawRequest = abi.decode(userOp.paymasterAndData[20:], (WithdrawRequest));
        uint256 withdrawAmount = withdrawRequest.amount;

        if (withdrawAmount < maxCost) {
            revert RequestLessThanGasMaxCost(withdrawAmount, maxCost);
        }

        if (withdrawRequest.asset != address(0)) {
            revert UnsupportedPaymasterAsset(withdrawRequest.asset);
        }

      ->_validateRequest(userOp.sender, withdrawRequest);

        bool sigFailed = !isValidWithdrawSignature(userOp.sender, withdrawRequest);
        validationData = (sigFailed ? 1 : 0) | (uint256(withdrawRequest.expiry) << 160);

        if (address(this).balance < withdrawAmount) {
            revert InsufficientBalance(withdrawAmount, address(this).balance);
        }

        _withdrawableETH[userOp.sender] += withdrawAmount - maxCost;
        context = abi.encode(maxCost, userOp.sender);
    }
```
EntryPoint then executes `UserOperation.callData` on SCW, which invokes `withdraw()` on MagicSpend.sol. The same `withdrawRequest` is passed in as a parameter.
```solidity
    function withdraw(WithdrawRequest memory withdrawRequest) external {
      ->_
User might withdraw less than initially calculated through the postOp function
Audit: 2024-03-coinbase-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
In the function `postOp` in the MagicSpend function at https://github.com/code-423n4/2024-03-coinbase/blob/main/src/MagicSpend/MagicSpend.sol#L143

`actualGasCost` is part of the inputs and is used in the line https://github.com/code-423n4/2024-03-coinbase/blob/main/src/MagicSpend/MagicSpend.sol#L156 to subtract from the maxGasCost.

There is no check to validate that `actualGasCost < maxGasCost` which would lead to a negative value added to the withdrawal amount, causing the withdrawable value < initially calculated value from the `validatePaymasterUserOp` function.


## Proof of Concept
The issue is found here

```solidity
uint256 withdrawable = _withdrawableETH[account] + (maxGasCost - actualGasCost);
```

## Tools Used
Manual Review

## Recommended Mitigation Steps
Add a check to see if `actualGasCost < maxGasCost`


## Assessed type

Other
Entry Point can't called a whitelisted set of functions 
Audit: 2024-03-coinbase-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
EntryPoint can call the executeWithoutChainIdValidation function. However, when executing this function, canSkipChainValidation is called to check whether the function selector matches the selectors of the whitelisted functions (`addOwnerAddress()`,`removeOwnerAtaddOwnerAddressIndex()`, `upgradeToAndCall()`,`addOwnerPublicKey()`), that EntryPoint when returning the value `true` cannot be called due to `onlyOwner`, `onlyProxy` modifiers. 

## Proof of Concept

```
    function addOwnerPublicKey(bytes32 x, bytes32 y) public virtual onlyOwner {...}
```
```
  function addOwnerAddress(address owner) public virtual onlyOwner {...}
```
```
    function removeOwnerAtIndex(uint256 index) public virtual onlyOwner {...}
```

```
    function upgradeToAndCall(address newImplementation, bytes calldata data)
        public
        payable
        virtual
        onlyProxy
    {...}
```
## Tools Used
Manual
## Recommended Mitigation Steps
Use `onlyEntryPointOrOwner` instead of `onlyOwner`



## Assessed type

Other
`Public key` owner can not add/remove other owners
Audit: 2024-03-coinbase-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-coinbase/blob/main/src/SmartWallet/MultiOwnable.sol#L85-L87
https://github.com/code-423n4/2024-03-coinbase/blob/main/src/SmartWallet/MultiOwnable.sol#L93-L95
https://github.com/code-423n4/2024-03-coinbase/blob/main/src/SmartWallet/MultiOwnable.sol#L102-L110


# Vulnerability details

# Impact
An owner, which is stored via it's public key ('x' & 'y' value), can not add/remove other owners.

# Proof of Concept
### Examples:
- If initally a 'Smart Wallet' has 2 owners, like in the tests for the 'MultiOwnable' contract, one of which is an encoded address and the other an encoded public key, the 'owner' represented by its public key can not add or remove other owners.

- If at deployment the 'Smart Wallet' has only a single owner, who is stored as a ('x', 'y') pair and the owner wants to add other addresses/owners for safety measures(losing his/her private key), it can not

The following test can be pasted in the `test/SmartWallet/MultiOwnable/AddOwnerPublicKey.t.sol` file

### Test:
```
function testAddOwnerWithPublicKey() public {
        /* Derive ethereum address from demo public key (x & y coordinates) */
        address _owner2Address = address(bytes20(keccak256(owner2Bytes) << 96));
        emit log_address(_owner2Address);
        assertEq(_owner2Address, 0xb97AA4eAe3A41D0B361Bd3e95FEB5e2A58226e63);

        vm.startPrank(_owner2Address);
        vm.expectRevert(MultiOwnable.Unauthorized.selector);
        mock.addOwnerAddress(abi.decode(owner1Bytes, (address)));

        vm.expectRevert(MultiOwnable.Unauthorized.selector);
        mock.addOwnerPublicKey(bytes32(uint256(2)), bytes32(uint256(5)));

        vm.expectRevert(MultiOwnable.Unauthorized.selector);
        mock.removeOwnerAtIndex(0);
    }
```

# Tools Used
Manual Review














## Assessed type

Other
`postOp` clears the whole `_withdrawableETH[account]` even though there might be some remaining there
Audit: 2024-03-coinbase-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

There's nowhere guaranteed that `withdrawable` (which is transferred to the end user) is the same as `_withdrawableETH[account]`.

Function `postOp()` clears `_withdrawableETH[account]` mapping after sending `withdrawable` to the user. Whenever `withdrawable < _withdrawableETH[account]` - the fund loss occurs, since the remaining `_withdrawableETH[account] - withdrawable` will be cleared and user won't be able to receive it back during another `postOp()` call.

## Proof of Concept

[File: MagicSpend.sol](https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/MagicSpend/MagicSpend.sol#L156-L162)
```
        uint256 withdrawable = _withdrawableETH[account] + (maxGasCost - actualGasCost);

        // Send the all remaining funds to the user accout.
        delete _withdrawableETH[account];
        if (withdrawable > 0) {
            SafeTransferLib.forceSafeTransferETH(account, withdrawable, SafeTransferLib.GAS_STIPEND_NO_STORAGE_WRITES);
        }
```

Firstly let's notice that `withdrawable = _withdrawableETH[account] + (maxGasCost - actualGasCost)`.
When function `postOp()` executes, `withdrawable` is being transferred to the user and mapping `_withdrawableETH[account]` is being cleared.
This constitutes an issue when `withdrawable < _withdrawableETH[account]`, because user won't be able to transfer remaining `_withdrawableETH[account] - withdrawable` later (`_withdrawableETH[account]` is cleared at the end of `postOp()` execution, thus at another `postOp()` call `_withdrawableETH[account]` will be 0).

1. Let's consider a scenario where `withdrawable < _withdrawableETH[account]`.
2. `postOp()` sends `withdrawable` to the user
3. User still has `_withdrawableETH[account] - withdrawable` to receive.
4. `postOp()` clears  `_withdrawableETH[account]`: `delete _withdrawableETH[account]`
5. User cannot get the remaining `_withdrawableETH[account] - withdrawable` - thus fund loss occurs.

## Tools Used

Manual code review


## Recommended Mitigation Steps

Change:

```
delete _withdrawableETH[account];
```

to:

```
_withdrawableETH[account] -= withdrawable;
```


## Assessed type

Other
Incomplete EIP-1967 Standard Implementation in CoinbaseSmartWallet
Audit: 2024-03-coinbase-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Incomplete EIP-1967 Standard Implementation in CoinbaseSmartWallet which could break protocol due to difficulty in Interacting with the Contract and lack of Transparency as ERC-1967 standardizes the storage slots used by proxy contracts, which allows block explorers and other services to correctly display information about the contract to end users. Not following the standard would cause wrong contract’s information display, leading to a lack of transparency
## Proof of Concept
According to the eip-1967 standards at https://eips.ethereum.org/EIPS/eip-1967 necessary factors are important such as 
- Logic contract address: The address of the logic contract that the proxy delegates to.
- Beacon contract address: The address of the beacon contract that returns the address of the logic contract for all proxies.
- Admin address: The address of the admin who has the authority to upgrade the proxy1.
But the Protocol does not follow due process in this standard implementation and should be corrected
## Tools Used
Manual Review ,https://eips.ethereum.org/EIPS/eip-1967
## Recommended Mitigation Steps
Protocol should make necessary adjustments to follow eip-1967 standards


## Assessed type

Context
The WebAuthn library is not robust enough and can potentially enable replay attacks
Audit: 2024-03-coinbase-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-coinbase/blob/main/src/WebAuthnSol/WebAuthn.sol#L121-L128


# Vulnerability details

## Impact
WebAuthn doesn't have enough checks to ensure that generated challenges are secure enough.

This challenge is used to verify the signature, but if it isn't generated with enough entropy, it will enable replay attacks.

## Disclaimer
**[Webauthn-sol](https://github.com/base-org/webauthn-sol) will be distributed as a library for public use.**

Even if the `CoinbaseSmartWallet`s are secure due to how this library is used in the codebase, this doesn't protect other users that might use it improperly in their own project.

This issue describes a standalone usage of this library, as the library itself is in scope, according to the README.

## Proof of Concept

According to the [webauthn paper](https://www.w3.org/TR/webauthn-2/#sctn-cryptographic-challenges), challenges **MUST** be generated with enough entropy to prevent replay attacks:

> In order to prevent replay attacks, the challenges MUST contain enough entropy to make guessing them infeasible. Challenges SHOULD therefore be at least 16 bytes long.

The issue is that the current implementation only checks that the client's challenge length matches the expected challenge (they could even be zero bytes in size):

```solidity
    // 12. Verify that the value of C.challenge equals the base64url encoding of options.challenge.
    bytes memory expectedChallenge = bytes(
        string.concat('"challenge":"', Base64.encodeURL(challenge), '"')
    );
    string memory actualChallenge = webAuthnAuth.clientDataJSON.slice(
        webAuthnAuth.challengeIndex,
        webAuthnAuth.challengeIndex + expectedChallenge.length
    ); 

    if (keccak256(bytes(actualChallenge)) != keccak256(expectedChallenge)) {
        return false;
    }	
```
https://github.com/code-423n4/2024-03-coinbase/blob/main/src/WebAuthnSol/WebAuthn.sol#L121-L128

This is not sufficient, and it enables possible replay attacks to anyone that uses this library.

## Tools Used
Manual review

## Recommended Mitigation Steps
Consider ensuring that the challenge was generated with enough entropy, for example by checking that its length is at least 16 bytes:

```diff
+	// Ensure that challenge was generated with enough entropy: 
+	// https://www.w3.org/TR/webauthn-2/#sctn-cryptographic-challenges
+	if (challenge.length < 16) {
+	    return false;
+	}
	// 12. Verify that the value of C.challenge equals the base64url encoding of options.challenge.
	bytes memory expectedChallenge = bytes(
	    string.concat('"challenge":"', Base64.encodeURL(challenge), '"')
	);
	string memory actualChallenge = webAuthnAuth.clientDataJSON.slice(
	    webAuthnAuth.challengeIndex,
	    webAuthnAuth.challengeIndex + expectedChallenge.length
	); 

	if (keccak256(bytes(actualChallenge)) != keccak256(expectedChallenge)) {
	    return false;
	}	
```	


## Assessed type

Invalid Validation