xanoutas/solidity-security-findings huggingface dataset:
Slippage controls for calling `PrizeVault::deposit` and `PrizeVault::mint`  functions are missing
Audit: 2024-03-pooltogether-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-pooltogether/blob/main/pt-v5-vault/src/PrizeVault.sol#L475-L479
https://github.com/code-423n4/2024-03-pooltogether/blob/main/pt-v5-vault/src/PrizeVault.sol#L482-L486


# Vulnerability details

`https://eips.ethereum.org/EIPS/eip-4626` mentions that "if implementors intend to support EOA account access directly, they should consider adding an additional function call for deposit/mint/withdraw/redeem with the means to accommodate slippage loss or unexpected deposit/withdrawal limits, since they have no other means to revert the transaction if the exact output amount is not achieved."

Using the PrizeVault contract that inherits the ERC4626DepositOnly contract, EOAs can call the ERC4626DepositOnly.deposit and ERC4626DepositOnly.mint functions directly. However, because no slippage controls can be specified when calling these functions, these function's shares and assets outputs can be less than expected to these EOAs.

## Impact
These function's shares and assets outputs can be less than expected to these EOAs.

## Proof of Concept
The following steps can occur for the described scenario involving the PrizeVault contract's ERC4626DepositOnly.mint function. The case involving the PrizeVault contract's ERC4626DepositOnly.deposit function is similar to this:

Alice wants to mint 1e18 shares in exchange for sending and burning 1e18 tokens.
Alice calls the contract's ERC4626DepositOnly.mint function with the shares input being 1e18.
Yet, such ERC4626DepositOnly.mint function call causes 1.2e18 tokens to be transferred from Alice.
Alice unexpectedly sends, burns, and loses 0.2e18 more tokens than expected for minting 1e18 shares.

## Tools Used
Manual review

## Recommended Mitigation Steps
The PrizeVault contract can be updated to include a deposit function that allows msg.sender to specify the minimum shares to be minted for calling the corresponding ERC4626DepositOnly.deposit function; calling such PrizeVault.deposit function should revert if the corresponding ERC4626DepositOnly.deposit function's shares output is less than the specified minimum shares to be minted. Similarly, the PrizeVault contract can also include a mint function that allows msg.sender to specify the maximum tokens to be sent for calling the corresponding ERC4626DepositOnly.mint function; calling such PrizeVault.mint function should revert if the corresponding ERC4626DepositOnly.mint function's assets output is more than the specified maximum tokens to be sent.


## Assessed type

ERC4626
Loss of fund as gas fund by user if 0 shares is minted in the yieldvault 
Audit: 2024-03-pooltogether-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


## Impact

The `_depositAndMint` function lacks a critical check after minting shares to ensure that the minted amount is more than zero. This design flaw allows the minting of zero shares to the yield vault, resulting in the emission of zero yield shares. Users are charged a gas fee for a transaction that does not yield any meaningful result, leading to potential financial inefficiency and user dissatisfaction.

## Proof of Concept

1. When the condition `_shares == 0` is met, the function reverts with the `MintZeroShares` error.

2. However, after minting shares, there is no subsequent check to verify that the minted amount is greater than zero.

3. Consequently, if `yield _shares` is zero, the transaction proceeds, emits zero yield shares, and charges the user a gas fee for a transaction with no meaningful impact.

### Code Reference

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

## Tools Used

Manual code analysis.

## Recommended Mitigation Steps

To address the issue and enhance the user experience, it is crucial to implement a check after minting shares to ensure that the minted amount is more than zero. This prevents users from incurring gas fees for transactions that do not result in meaningful yield shares.

Update the code as follows:

```solidity
// After minting shares, check that the minted amount is more than zero
uint256 _yieldVaultShares = yieldVault.previewDeposit(_assetsWithDust);
uint256 _assetsUsed = yieldVault.mint(_yieldVaultShares, address(this));
if (_assetsUsed == 0) {
    revert MintZeroYieldShares();
}

// 
```

By incorporating this adjustment, the function ensures that users are not charged gas fees for transactions that emit zero yield shares. 





## Assessed type

Invalid Validation
A variable indicating the number of vaults in existence should be there in the factory contract.
Audit: 2024-03-pooltogether-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Due to security purposes, a variable (uint256) should exist as a counter for total number of vaults. It will be incremented when `deployVault()` is called and its value will be returned directly when `totalVaults()` will be called.

## Proof of Concept


## Tools Used
Manual review

## Recommended Mitigation Steps
this can be used
```
uint256 vaultCounter = 0;

function deployVault(
      params
    ) external returns (PrizeVault) {
       ...
        allVaults.push(_vault);
        vaultCounter++;
       ...
    }

function totalVaults() external view returns (uint256) {
        return vaultCounter;
// this function also saves gas as we are reading the value from a state variable
    }
```


## Assessed type

Other
The winner can steal claimer fees, and force him to pay for the gas
Audit: 2024-03-pooltogether-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-pooltogether/blob/main/pt-v5-vault/src/abstract/Claimable.sol#L86-L92


# Vulnerability details

## Impact
When the winner earns his reward he can either claim it himself, or he can let a claimer contract withdraw it on his behalf, and he will pay part of his reward for that. This is because the user will not pay for the gas fees, instead the claimer contract will pay it instead.

The problem here is that the winner can make the claimer pay for the gas of the transaction, without paying the fees that the claimer contract takes.

Claimer contracts are allowed for anyone to use them, transfer prizes to winners, and claim some fees. where the one who fired the transaction is the one who will pay for the fees, so he deserved that fees.

[pt-v5-claimer/Claimer.sol#L120-L150](https://github.com/GenerationSoftware/pt-v5-claimer/blob/main/src/Claimer.sol#L120-L150)
```solidity
  // @audit and one can call the function
  function claimPrizes( ... ) external returns (uint256 totalFees) {
    ...

    if (!feeRecipientZeroAddress) {
      ...
    }

    return feePerClaim * _claim(_vault, _tier, _winners, _prizeIndices, _feeRecipient, feePerClaim);
  }
```

As in the function, the function takes winners, and he passed the fees recipient and his fees (but it should not exceeds the maxFees which is initialzed in the constructor).

Now We know that anyone can transfer winners' prizes and claim some fees.

---

Before the prizes are claimed, the winner can initialize a hook before calling the `PoolPrize::claimPrize`, and this is used if the winner wants to initialize another address as the receiver of the reward.

The hook parameter is passed by parameters that are used to determine the correct winner (winner address, tier, prizeIndex).

[abstract/Claimable.sol#L85-L95](https://github.com/code-423n4/2024-03-pooltogether/blob/main/pt-v5-vault/src/abstract/Claimable.sol#L85-L95)
```solidity
    uint24 public constant HOOK_GAS = 150_000;

    ...

    function claimPrize( ... ) external onlyClaimer returns (uint256) {
        address recipient;

        if (_hooks[_winner].useBeforeClaimPrize) {
            recipient = _hooks[_winner].implementation.beforeClaimPrize{ gas: HOOK_GAS }(
                _winner,
                _tier,
                _prizeIndex,
                _reward,
                _rewardRecipient
            );
        } else {
            recipient = _winner;
        }

        if (recipient == address(0)) revert ClaimRecipientZeroAddress();

        uint256 prizeTotal = prizePool.claimPrize( ... );
      
        ...
    }

```

But to prevent `OOG` the gas is limited to `150K`.

---

Now What can the user do to make the claimer pay for the transaction, and not pay any fees is:

- he will make a `beforeClaimPrize` hook
- In this function, the user will simply claim his reward `Claimer::claimPrizes(...params)` but with settings no fees, and only passing his winning prize parameters (we got
PrizeVault is incompatible with rebasing tokens
Audit: 2024-03-pooltogether-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Some tokens (eg. AMPL), known as rebasing tokens, have dynamic balances. This means that the token balance of an address could increase or decrease over time.

However, the `PrizeVault` contract is unable to handle such changes in token balance. When users call `PrizeVault.deposit()`, the number of shares they will have will equal the deposited asset amount. Upon eventually redeeming their shares by calling `PrizeVault.redeem()`, they will receive the same amount of token or less than what they have deposited. 
The vault can not return more tokens than what has been deposited by the user.
In the case of increasing token balance, users won't receive the increased amount.
In the case of a decreased token balance, `PrizeVault` contract will return decreased assets to every user, and every user will get the same percentage of decrease, no matter the time of deposition.

Therefore, users will lose funds if they deposit into a `PrizeVault` with a rebasing token as the asset.


## Tools Used
Manual Review

## Recommended Mitigation Steps
Consider implementing a token blacklist in the protocol, and adding all rebasing tokens to this blacklist.

Additionally, consider documenting that the protocol is not compatible with rebasing tokens.


## Assessed type

ERC20
`PrizeVault` is not fully compliant with the ERC4626 standard
Audit: 2024-03-pooltogether-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Vulnerability Details
The 4626 EIP specifies that in order for a contract to be fully compliant with the standard, the `convertToShares` function must not revert, except due to an integer overflow. The issue is that it is currently possible for `PrizeVault`'s `convertToShares` function to revert due to a division by 0.
As if the total assets are less than the total debt, the function returns `_assets.mulDiv(totalDebt_, _totalAssets, Math.Rounding.Down);`. Therefore, if `_totalAssets` is 0 and `totalDebt_` is more than 0 the function will revert. This is possible in the case that all users have withdrawn their deposits, but the fee recipient is yet to claim their yield fees and `yieldFeeBalance` is more than 0. The fact that `yieldFeeBalance` is more than 0 does not mean that the total assets will be more than 0, due to the fact that `_totalAssets` can always be decreased because of fees, slippage, or loss of funds in the underlying yield vault.

## Impact
`PrizeVault` is not fully compliant with the ERC4626 standard.

## Tools Used
Manual review

## Recommended Mitigation Steps
In `converToShares` if `_totalAssets` is 0 and `totalDebt_` is a positive number return 0.


## Assessed type

ERC4626
PrizeVault is not fully complient with EIP4626
Audit: 2024-03-pooltogether-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

`PrizeVault` doesn't strictly follow the rules for an EIP4626 contract. The following functions are non-compliant:
- convertToAssets
- maxDeposit
- maxMint

## Proof of Concept

EIP4626 states the following:

> convertToAssets MUST NOT revert unless due to integer overflow caused by an unreasonably large input.

However we can see that in `PrizeVault::convertToAssets` will revert if `totalDebt_` is 0 (due to division by zero error). This can happen for example if all the users withdrew their balances and claimed rewards and `yieldFeeBalance` > `_asset.balanceOf(address(this))`.

>  maxDeposit MUST NOT rely on balanceOf of asset

And we can see the `maxDeposit` function in `PrizeVault`:

    function maxDeposit(address) public view returns (uint256) {
        uint256 _totalSupply = totalSupply();
        uint256 totalDebt_ = _totalDebt(_totalSupply);
        if (totalAssets() < totalDebt_) return 0;

        // the vault will never mint more than 1 share per asset, so no need to convert supply limit to assets @audit is it true??
        uint256 twabSupplyLimit_ = _twabSupplyLimit(_totalSupply);
        uint256 _maxDeposit;
        uint256 _latentBalance = _asset.balanceOf(address(this));
        uint256 _maxYieldVaultDeposit = yieldVault.maxDeposit(address(this));
        if (_latentBalance >= _maxYieldVaultDeposit) {
            return 0;
        } else {
            unchecked {
                _maxDeposit = _maxYieldVaultDeposit - _latentBalance;
            }
            return twabSupplyLimit_ < _maxDeposit ? twabSupplyLimit_ : _maxDeposit;
        }
    }

And indeed it depends on `_latentBalance` which is the balanceOf.

> maxDeposit MUST return the maximum amount of assets deposit would allow to be deposited for receiver and not cause a revert

There is no check in `PrizeVault::deposit` or `PrizeVault::mint` to check that users don't deposit more than `maxDeposit` which opposes the EIP.

The same goes for `maxMint`.

## Tools Used

Manual Review

## Recommended Mitigation Steps

Review https://eips.ethereum.org/EIPS/eip-4626 and follow the stated rules.


## Assessed type

ERC4626
The yield buffer acts as a constant margin against a proportional lossy deposit error
Audit: 2024-03-pooltogether-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details



## Impact
DoS of deposits.

## Proof of Concept
When depositing it is [required that `totalAssets() >= totalDebt()`](https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L874
). `totalAssets()` increases over `totalDebt()` when yield is generated, but this yield is constantly withdrawn, up to leaving the yield buffer. This yield buffer, which is intended to be very small, is the only margin in this inequality.
However,
```solidity
function totalAssets() public view returns (uint256) {
    return yieldVault.convertToAssets(yieldVault.balanceOf(address(this))) + _asset.balanceOf(address(this));
}
```
is only approximate since `yieldVault.convertToAssets()` is approximate, per EIP-4626. And the error of `totalAssets()` may be proportional to the value of `totalAssets()`, for example if a price oracle is used in the yield vault.

As more and more is deposited into the PrizeVault, `totalAssets()` and `totalDebt()` will both increase equally, but since the yield buffer is constant their difference will stay the same. Eventually the error in `totalAssets()` will exceed the yield buffer difference between them, and thus break the inequality condition. This DoS-es deposits.

## Recommended Mitigation Steps
The yield buffer can certainly be used to account for small drops in `totalAssets()` due to noise/errors. Allow the `yieldBuffer` to be increased/decreased by the owner.
(Another reason it should not be immutable is that the initial conditions used to decide its value may change further down the line, e.g. because the exchange rate increases.)


## Assessed type

ERC4626
`_maxYieldVaultWithdraw()` uses `yieldVault.convertToAssets()`
Audit: 2024-03-pooltogether-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details



## Impact
`maxWithdraw()` and `maxRedeem()` may return too much, violating EIP-4626. `liquidatableBalanceOf()` may return too much.

## Proof of Concept
```solidity
function _maxYieldVaultWithdraw() internal view returns (uint256) {
    return yieldVault.convertToAssets(yieldVault.maxRedeem(address(this)));
}
```
uses `yieldVault.convertToAssets()` which per EIP-4626 is only approximate. Especially, it might return too much, and thus `_maxYieldVaultWithdraw()` might return too much.
`_maxYieldVaultWithdraw()` is used [in `maxWithdraw()`](https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L405), [in `maxRedeem()`](https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L416), and [in `liquidatableBalanceOf()`](https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L639) which functions may thus return too much. In the case of `maxWithdraw()` and `maxRedeem()` this violates EIP-4626.

## Recommended Mitigation Steps
Use `yieldVault.previewRedeem(yieldVault.maxRedeem(address(this)))`.


## Assessed type

ERC4626
`maxDeposit()` uses `yieldVault.maxDeposit()` but `_depositAndMint()` uses `yieldVault.mint()`
Audit: 2024-03-pooltogether-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details



## Impact
`maxDeposit()` might return a value greater than can be deposited, violating EIP-4626.

## Proof of Concept
[`maxDeposit()` returns up to `yieldVault.maxDeposit(address(this))`](https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L374-L392). However, [`_depositAndMint()` deposits using `yieldVault.mint()`](https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L866) which may have a stricter limit than `yieldVault.deposit()`. In that case depositing `maxDeposit()` would revert, which violates EIP-4626.

## Recommended Mitigation Steps
Use `yieldVault.previewRedeem(yieldVault.maxMint())`.


## Assessed type

ERC4626
No written limitations on a total supply, that should not be higher than uint96max value 
Audit: 2024-03-pooltogether-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
There is no check that after minting a new token the total supply

## Proof of Concept
`PrizeVault.sol`
```solidity
function totalSupply() public view virtual override(ERC20) returns (uint256) {
        return twabController.totalSupply(address(this));
}
function maxDeposit(address) public view returns (uint256) {
   ...
   uint256 _totalSupply = totalSupply();
   uint256 twabSupplyLimit_ = _twabSupplyLimit(_totalSupply);
   ...
}
function _twabSupplyLimit(uint256 _totalSupply) internal pure returns (uint256) {
        unchecked {
            return type(uint96).max - _totalSupply;
        }
    }
```
So here there will be a problem if `twabController.totalSupply(address(this));` will return a value higher than type(uint96).max, `_twabSupplyLimit` will always revert

But In `TwabController.sol`(OOS) there are no limitations while minting new tokens, so at some point there **might** be a case when the balance of a token on Vault will be higher than `uint96Max`.  

## Tools Used
Manual review

## Recommended Mitigation Steps
Add a custom error, or take in case such a scenario


## Assessed type

Math
No asset buffer to account for withdrawal rounding errors
Audit: 2024-03-pooltogether-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details



## Impact
Withdrawals might revert.

## Proof of Concept
In PrizeVault:
```solidity
function _withdraw(address _receiver, uint256 _assets) internal {
    // The vault accumulates dust from rounding errors over time, so if we can fulfill the withdrawal from the
    // latent balance, we don't need to redeem any yield vault shares.
    uint256 _latentAssets = _asset.balanceOf(address(this));
    if (_assets > _latentAssets) {
        // The latent balance is subtracted from the withdrawal so we don't withdraw more than we need.
        uint256 _yieldVaultShares = yieldVault.previewWithdraw(_assets - _latentAssets);
        // Assets are sent to this contract so any leftover dust can be redeposited later.
        yieldVault.redeem(_yieldVaultShares, address(this), address(this));
    }
    if (_receiver != address(this)) {
        _asset.transfer(_receiver, _assets);
    }
}
```
Per EIP-4626 `previewWithdraw()` returns no less than the number of shares that would be burned in a `withdraw()`, while `redeem()` must round down (the vault must always be favoured). There is no guarantee that these opposing errors will perfectly cancel out, but either may dominate, depending on implementation and current values. Thus `yieldVault.redeem(_yieldVaultShares)` might return less than `_assets - _latentAssets` assets, i.e. too few shares were redeemed to bring in the assets required. Then ` _asset.transfer(_receiver, _assets)` will revert.

## Recommended Mitigation Steps
Currently the yield buffer is kept in the form of yield vault shares as much as possible by always trying to deposit the remaining assets. Make sure that some amount is also always kept in the form of assets, and deduct this buffer amount from the `_latentAssets` calculation here.


## Assessed type

Other