xanoutas/solidity-security-findings huggingface dataset:
Init Functions Are Susceptible To Front-running
Audit: 2024-03-coinbase-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
The `initialize()` functions below are not called by another contract atomically after the contract is deployed, so it's possible for a malicious user to call `initialize()` which, if it's noticed in time, would require the project to re-deploy the contract in order to properly initialize.

## Proof of Concept
```solidity=114
    function initialize(bytes[] calldata owners) public payable virtual {
        if (nextOwnerIndex() != 0) {
            revert Initialized();
        }

        _initializeOwners(owners);
    }
```

## Tools Used
VS

## Recommended Mitigation Steps
We recommend creating a factory contract, which will `new` and `initialize()` each contract atomically.


## Assessed type

Access Control
Lack of Storage Gap in Upgradeable Contract
Audit: 2024-03-coinbase-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
There is no storage gap preserved in the logic contract `CoinbaseSmartWallet`. Any logic contract that acts as a base contract inherited by other upgradeable contracts should have a reasonable size of storage gap preserved for potential new state variables introduced in future upgrades. Otherwise, there could be a potential storage slot collision.

```solidity=20
   contract CoinbaseSmartWallet is MultiOwnable, UUPSUpgradeable, Receiver, ERC1271 {
```

## Proof of Concept


## Tools Used
VS

## Recommended Mitigation Steps
It is prudent to reserve a storage gap of an appropriate size in the base contract for enhanced flexibility in future upgrades of the logic contract. This is achieved by declaring a fixed-size array, typically of `uint256` elements, each occupying a 32-byte slot, in the base contract. Label this array with the identifier `__gap` or any name prefixed with `__gap_` to indicate its purpose as a reserved space.

More detailed info : 
- https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps


## Assessed type

Upgradable
Potential Contract Balance Drain Due to Reentrant Calls in withdrawGasExcess
Audit: 2024-03-coinbase-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
The reentrancy vulnerability in `withdrawGasExcess` could potentially allow an attacker to repeatedly withdraw funds before the state changes are finalized. This could lead to a drain of the contract's balance and disrupt its intended functionality.


## Proof of Concept
The `withdrawGasExcess` function in the `MagicSpend` contract suffers from a reentrancy risk due to the sequence of operations within the function. Here's a detailed breakdown:
```solidity
function withdrawGasExcess() external {
    uint256 amount = _withdrawableETH[msg.sender];
    if (amount == 0) revert NoExcess();

    delete _withdrawableETH[msg.sender];  // <-- State change
    _withdraw(address(0), msg.sender, amount);  // <-- External call
}
```
The function starts by checking if there are any funds available for withdrawal. If funds exist, the `_withdrawableETH` mapping for the caller is deleted, indicating that the funds have been withdrawn. Then, a transfer of funds to the caller's address is initiated.

However, the vulnerability lies in the order of operations. After deleting the `_withdrawableETH[msg.sender]` entry, the function makes an external call to `_withdraw`, transferring funds to the caller's address. This leaves the contract susceptible to reentrancy attacks.
## Tools Used
Manual
## Recommended Mitigation Steps
Withdrawal operation should be moved before deleting the `_withdrawableETH[msg.sender]` entry. This ensures that the state changes are completed before any external calls are made. Additionally, implementing a reentrancy guard such as the `ReentrancyGuard` pattern can provide an extra layer of protection against reentrancy attacks.


## Assessed type

Reentrancy
The expire of the withdraw signature is not been validated.
Audit: 2024-03-coinbase-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

When a user request to the paymaster gas to pay for his transaction they get the a signature with the values below:

```
 struct WithdrawRequest {
        /// @dev The signature associated with this withdraw request.
        bytes signature;
        /// @dev The asset to withdraw. NOTE: Only ETH (associated with zero address) is supported for now.
        address asset;
        /// @dev The requested amount to withdraw.
        uint256 amount;
        /// @dev Unique nonce used to prevent replays.
        uint256 nonce;
        /// @dev The maximum expiry the withdraw request remains valid for.
        uint48 expiry;
    }
```

The Problem is that the expiry is not been checked whether is expired or not.

```
 validationData = (sigFailed ? 1 : 0) | (uint256(withdrawRequest.expiry) << 160);  <------
```
[[Link]](https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/MagicSpend/MagicSpend.sol#L128C8-L128C89)

## Impact
Expired transaction can get processed the contract

## Proof of Concept

Run the next test in `file:/test/MagicSpend/ValidatePaymasterUserOp.t.sol`

```
 function test_expire() public {
        MagicSpend.WithdrawRequest memory WithdrawRequest = MagicSpend.WithdrawRequest({
            asset: asset,
            amount: 1e30,
            nonce: nonce,
            expiry: uint48(block.timestamp) - 1, // block timestamp - 1
            signature: _getSignature()
        });

        UserOperation memory userOp = UserOperation({
            sender: withdrawer,
            nonce: 0,
            initCode: "",
            callData: "",
            callGasLimit: 49152,
            verificationGasLimit: 378989,
            preVerificationGas: 273196043,
            maxFeePerGas: 1000304,
            maxPriorityFeePerGas: 1000000,
            paymasterAndData: abi.encodePacked(address(magic), abi.encode(WithdrawRequest)),
            signature: ""
        });

        (bytes memory context, uint256 validationData) = magic.validatePaymasterUserOp(userOp, userOpHash, maxCost); // the validation is passed even with the transaction expiry
    }

```

## Tools Used
Manual, Foundry

## Recommended Mitigation Steps
Consider check if the transaction is expired.


## Assessed type

Other
The `claimYieldFeeShares` function will lose shares if `shares` != `yieldFeeBalance`
Audit: 2024-03-pooltogether-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L611


# Vulnerability details


### [M-1] The `claimYieldFeeShares` function will lose shares if `shares` != `yieldFeeBalance`

**Description** There is a mishandling of `yieldFeeBalance` in the `claimYieldFeeShares` function. this function will set the `yieldFeeBalance` to zero every time is called and doesn't handle the shares well:
```java
    function claimYieldFeeShares(uint256 _shares) external onlyYieldFeeRecipient {
        if (_shares == 0) revert MintZeroShares();

        uint256 _yieldFeeBalance = yieldFeeBalance;
        if (_shares > _yieldFeeBalance) revert SharesExceedsYieldFeeBalance(_shares, _yieldFeeBalance);

        // @audit-medium the `YieldFeeRecipient` lose shares if shares != yieldFeeBalance
        // it should be `yieldFeeBalance -= _shares;` instead for mitigation
        yieldFeeBalance -= _yieldFeeBalance;

        _mint(msg.sender, _shares);

        emit ClaimYieldFeeShares(msg.sender, _shares);
    }
```
the `claimYieldFeeShares` function must subtract `yieldFeeBalance` with `shares` not with itself because for example if `yieldFeeBalance` was 10e18 and the `yieldFeeRecipient` wants just to call the `claimYieldFeeShares` with `1e18` shares then he would lose the other `9e18` shares which could mint. he had this potential to mint shares more and just lost it.

**Impact** funds are not at direct risk but `yieldFeeRecipient` will just lose shares that he could minted.


**Recommend Mitigation**
```diff
-	yieldFeeBalance -= _yieldFeeBalance;
+	yieldFeeBalance -= _shares;
```


## Assessed type

Math
Yield Fee Shares could be lost while claiming claimYieldFeeShares
Audit: 2024-03-pooltogether-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-03-pooltogether/blob/main/pt-v5-vault/src/PrizeVault.sol#L611
https://github.com/code-423n4/2024-03-pooltogether/blob/main/pt-v5-vault/src/PrizeVault.sol#L617


# Vulnerability details

## Impact
- Whenever `onlyYieldFeeRecipient` calls `claimYieldFeeShares(uint256 _shares)`, 
  if _shares < total shares accrued from yield fee balance then there will be 
  loss of yieldFeeBalance and that yieldFeeBalance could never be claimed back.  

- There is no check in `claimYieldFeeShares()` that how many shares the claimer 
  wants to claim so according to that `yieldFeeBalance` should get subtracted, 
  but here whole yieldFeeBalance gets subtracted without checking how many shares 
  the claimer wants to claim


## Proof of Concept

- Assume vault has generated yield and we have liquidated it.
- A yield fee has been generated and it's in the form of shares.
- Now Bob who is YieldFeeReceipient claims the yield through 
  claimYieldFeeShares(uint256 _shares) by passing it only half of the total yield 
  fee shares .
- Bob get's minted half of the total shares.
- Bob can never claim back his other half of the shares as yieldFeeBalance 
  becoming zero without checking that Bob is trying to claim total or partial 
  amout of shares.
- Thus Bob can't claim his remaining half of the yield fee share.
```
function testClaimFeeBalance_Bug() public {
        vault.setYieldFeePercentage(1e8); // 10%
        vault.setYieldFeeRecipient(bob);
        assertEq(vault.totalDebt(), 0);

        // make an initial deposit
        underlyingAsset.mint(alice, 1e18);
        vm.startPrank(alice);
        underlyingAsset.approve(address(vault), 1e18);
        vault.deposit(1e18, alice);
        vm.stopPrank();

        assertEq(vault.totalAssets(), 1e18);
        assertEq(vault.totalSupply(), 1e18);
        assertEq(vault.totalDebt(), 1e18);

        // mint yield to the vault and liquidate
        underlyingAsset.mint(address(vault), 1e18);
        vault.setLiquidationPair(address(this));
        
        uint256 maxLiquidation = vault.liquidatableBalanceOf(address(underlyingAsset));
        
        uint256 amountOut = maxLiquidation / 2;
        
        uint256 yieldFee = (1e18 - vault.yieldBuffer()) / (2 * 10); // 10% yield fee + 90% amountOut = 100%
        console.log("the generated fee from liquidation ",yieldFee);
        vault.transferTokensOut(address(0), bob, address(underlyingAsset), amountOut);

        assertEq(vault.totalAssets(), 1e18 + 1e18 - amountOut); // existing balance + yield - amountOut
        assertEq(vault.totalSupply(), 1e18); // no change in supply since liquidation was for assets
        assertEq(vault.totalDebt(), 1e18 + yieldFee); // debt increased since we reserved shares for the yield fee

        // Claiming yield fee shares
        vm.startPrank(bob);
        console.log("Before claiming Yield fee balance  ",vault.yieldFeeBalance());
        
        vault.claimYieldFeeShares(yieldFee/2);
`claimYieldFeeShares()` sets `yieldFeeBalance = 0` instead of `yieldFeeBalance -= _shares`
Audit: 2024-03-pooltogether-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L617


# Vulnerability details



## Impact
Yield fees are lost.

## Proof of Concept
Yield fees are intermediately accounted for in `yieldFeeBalance`, later to be minted as normal shares by calling `claimYieldFeeShares()`:
```solidity
function claimYieldFeeShares(uint256 _shares) external onlyYieldFeeRecipient {
    if (_shares == 0) revert MintZeroShares();

    uint256 _yieldFeeBalance = yieldFeeBalance;
    if (_shares > _yieldFeeBalance) revert SharesExceedsYieldFeeBalance(_shares, _yieldFeeBalance);

    yieldFeeBalance -= _yieldFeeBalance;

    _mint(msg.sender, _shares);

    emit ClaimYieldFeeShares(msg.sender, _shares);
}
```
`claimYieldFeeShares(_shares)` mints `_shares` shares but always sets `yieldFeeBalance = 0` instead of `yieldFeeBalance -= _shares`, even when `_shares < yieldFeeBalance`. A subsequent call to claim the remaining `yieldFeeBalance - _shares` fees would therefore revert, i.e. the remaining yield fees are lost.

## Recommended Mitigation Steps
```diff
- yieldFeeBalance -= _yieldFeeBalance;
+ yieldFeeBalance -= _shares;
```


## Assessed type

Math
claimYieldFeeShares rests yieldFeeBalance every time even if not all fees are claimed
Audit: 2024-03-pooltogether-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-03-pooltogether/blob/main/pt-v5-vault/src/PrizeVault.sol#L614-L617


# Vulnerability details

## Impact

`PrizeVault::claimYieldFeeShares` can only be called by the `yieldFeeRecipient` to claim the fees. The `yieldFeeRecipient` can specify the exact amount of shares he wants to claim, however this is a problem because the `yieldFeeBalance` is reset every time, no matter how many shares the `yieldFeeRecipient` claimed.

## Proof of Concept

    function claimYieldFeeShares(uint256 _shares) external onlyYieldFeeRecipient {
        if (_shares == 0) revert MintZeroShares();

        uint256 _yieldFeeBalance = yieldFeeBalance;
        if (_shares > _yieldFeeBalance) revert SharesExceedsYieldFeeBalance(_shares, _yieldFeeBalance);
        // @audit this should be shares
        yieldFeeBalance -= _yieldFeeBalance;

        _mint(msg.sender, _shares);

        emit ClaimYieldFeeShares(msg.sender, _shares);
    }

We can see that first the `yieldFeeBalance ` is stored in `_yieldFeeBalance` and then subtracted with the same amount even if the `yieldFeeRecipient` doesn't claim all the fees.

This leaves unclaimed fees and affects the `_totalDebt` since it would return less than it actually is.

## Tools Used

Manual Review

## Recommended Mitigation Steps

Subtract the shares from `yieldFeeBalance` instead of reseting it.

        yieldFeeBalance -= _shares;


## Assessed type

Other
`claimYieldFeeShares` decrements `yieldFeeBalance` by wrong amount
Audit: 2024-03-pooltogether-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L611-L622


# Vulnerability details

## Impact
In `claimYieldFeeShares` the fee recipient can choose the amount of shares that they want to claim from the `yieldFeeBalance`. The issue is that currently, even if the fee recipient chooses a number less than the entire `yieldFeeBalance`, `yieldFeeBalance` is reset to 0, preventing the fee recipient from claiming any more of the fees they were initially granted.
For example, if the fee recipient chooses to claim only 50 of 100 from the `yieldFeeBalance`, `yieldFeeBalance` will be reset to 0, and the fee recipient will not be able to claim the remaining 50 yield fees. 

## Proof of Concept

As we can see `yieldFeeBalance` is decremented by `_yieldFeeBalance` instead of `_shares`.

https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L617C13-L617C45
```
   yieldFeeBalance -= _yieldFeeBalance;
```

## Tools Used
Manual review

## Recommended Mitigation Steps
Update from:

https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L617C13-L617C45
```
   yieldFeeBalance -= _yieldFeeBalance;
```

To:

```
   yieldFeeBalance -= _shares;
```


## Assessed type

Other
Yield fee is not accounted for during liquidation and therefore lost to the receiver in the `PrizeVault` contract
Audit: 2024-03-pooltogether-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-03-pooltogether/blob/main/pt-v5-vault/src/PrizeVault.sol#L690
https://github.com/code-423n4/2024-03-pooltogether/blob/main/pt-v5-vault/src/PrizeVault.sol#L692


# Vulnerability details

## Impact
Yield fee is not accounted for during liquidation in the `PrizeVault::transferTokensOut` function during such liquidations. Therefore, the yield fee percentage relative to the `amountOut` is lost.

## Proof of Concept
The function below executes liquidations in the `PrizeVault` contract. As intended by the PoolTogether protocol, the `_yieldFee` is to be accounted for in the amount being liquidated but in the current implementation, it does not ensure such liquidations get deducted the relative fee while allocating the supposed fee to the `yieldFeeBalance`.

```solidity
/// @inheritdoc ILiquidationSource
    /// @dev Emits a `TransferYieldOut` event
    /// @dev Supports the liquidation of either assets or prize vault shares.
    function transferTokensOut(
        address,
        address _receiver,
        address _tokenOut,
        uint256 _amountOut
    ) public virtual onlyLiquidationPair returns (bytes memory) {
        if (_amountOut == 0) revert LiquidationAmountOutZero();

        uint256 _availableYield = availableYieldBalance();
        uint32 _yieldFeePercentage = yieldFeePercentage;

        // Determine the proportional yield fee based on the amount being liquidated:
        uint256 _yieldFee;
        if (_yieldFeePercentage != 0) {
            // The yield fee is calculated as a portion of the total yield being consumed, such that 
            // `total = amountOut + yieldFee` and `yieldFee / total = yieldFeePercentage`. 
            _yieldFee = (_amountOut * FEE_PRECISION) / (FEE_PRECISION - _yieldFeePercentage) - _amountOut;
        }

        // Ensure total liquidation amount does not exceed the available yield balance:
        if (_amountOut + _yieldFee > _availableYield) {
            revert LiquidationExceedsAvailable(_amountOut + _yieldFee, _availableYield);
        }

        // Increase yield fee balance:
        if (_yieldFee > 0) {
            yieldFeeBalance += _yieldFee; // @note yieldFeeBalance accounts for the _yieldFee this transaction will incur
        }

        // Mint or withdraw amountOut to `_receiver`:
        if (_tokenOut == address(_asset)) {
            _withdraw(_receiver, _amountOut);  // @audit _yieldFee is not accounted for         
        } else if (_tokenOut == address(this)) {
            _mint(_receiver, _amountOut); // // @audit _yieldFee is not accounted for
        } else {
            revert LiquidationTokenOutNotSupported(_tokenOut);
        }

        emit TransferYieldOut(msg.sender, _tokenOut, _receiver, _amountOut, _yieldFee);

        return "";
    }
```

Breaking down the interesting part of the function logic, we can see that:

- `yieldFeeBalance` increments by the `_yieldFee` this liquidation will incur.

```solidity
if (_yieldFee > 0) {
Claiming yield fees may fully deplete the yield buffer and render the vault into a lossy state
Audit: 2024-03-pooltogether-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L611-L622


# Vulnerability details

## Vulnerability Details
Yield fees are accumulated when `transferTokensOut` is called. The function calculates the amount of available yield and takes out a percentage of that number, which is allocated for the yield fee recipient to claim.

The issue is that the number of available yield that is calculated in `transferTokensOut` is highly likely to be different from the available yield when the yield fee recipient calls `claimYieldFeeShares`. Therefore, the following issue may occur:
During periods where a lot of yield is earned from a yield vault, `transferTokensOut` is called which increases `yieldFeeBalance`. Instead of claiming the yield fees immediately the fee receiver decides to wait until more fees are accrued. Time goes by and the vault enters a lossy state which can happen due to a loss of funds in the underlying vault. Now the number of available yield is less than what it was when `transferTokensOut` was initially called. Now, if the fee receiver decides to claim the fees the yield buffer will be fully depleted and shares that are not backed up by any assets will be minted to the receiver.

## Impact
This will prevent any user from depositing to the vault for a long-lasting period of time and will cause all withdrawals to be at a loss.

## Tools Used
Manual review

## Recommended Mitigation Steps
When the fee recipient calls `claimYieldFeeShares` it should be made sure that there is enough available yield, or the logic of `claimYieldFeeShares` should be implemented inside of `transferTokensOut`.


## Assessed type

Other
the PriveVaultFactory doesn't transfer ownership to the msg.sender in the function deployVault
Audit: 2024-03-pooltogether-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVaultFactory.sol#L92


# Vulnerability details

## Impact
The protocol will be unable to call the `OnlyOwner` protected functions in the PrizeVault

## Proof of Concept
After the `PriveVaultFactory` deploys the `PrizeVault`, it doesn't transfer ownership of the contract to the msg.sender, retaining the ownership rights.
as the `PrizeVaultFactory` doesn't have the functionality to call functions protected by the `OnlyOwner` modifier, the protocol will be unable to call those functions.

## Tools Used
Manual Audit

## Recommended Mitigation Steps
transfer ownership to the `PrizeVault` deployer or add functionality in the `PriveVaultFactory` to call the setter functions


## Assessed type

Other