xanoutas/solidity-security-findings huggingface dataset:
Fee recipient may block users withdrawal if yieldFee is calculated within a certain range
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#L667


# Vulnerability details

## Impact

Users may not get their full withdrawal back.

## Proof of Concept

In `transferTokenOut()`, `yieldFee` is calculated. Firstly, `availableYieldBalance()` is called. 

`availableYieldBalance()` checks whether there is available yield in the vault to be withdrawn. It calls `_availableYieldBalance(totalAssets(), totalDebt())`

```
    function availableYieldBalance() public view returns (uint256) {
        return _availableYieldBalance(totalAssets(), totalDebt());
    }
```

`_availableYieldBalance()` calculates the available yield balance, and includes a buffer. Assume sDAI is the underlying yield vault, if 10 DAI is the available yield balance, and 1 DAI is the buffer, then 9 DAI will be the available yield balance instead. 

```
    function _availableYieldBalance(uint256 _totalAssets, uint256 totalDebt_) internal view returns (uint256) {
        uint256 totalYieldBalance_ = _totalYieldBalance(_totalAssets, totalDebt_);
        uint256 _yieldBuffer = yieldBuffer;
        if (totalYieldBalance_ >= _yieldBuffer) {
            unchecked {
>               return totalYieldBalance_ - _yieldBuffer;
            }
        } else {
            return 0;
        }
    }
```

`transferTokensOut()` then calculates the `yieldFee`, and checks whether the _amountOut + yieldFee is greater than the `availableYield`. Then, it increases the yield fee balance.

```
        if (_amountOut + _yieldFee > _availableYield) {
            revert LiquidationExceedsAvailable(_amountOut + _yieldFee, _availableYield);
        }
 // Increase yield fee balance:
        if (_yieldFee > 0) {
            yieldFeeBalance += _yieldFee;
        }
```

Assume availableYield is 99 DAI (supposed to be 100 DAI, but 1 DAI as buffer). Assume yieldFee is 10%.
- `_amountOut` cannot be 99 as yield fee will be 9.9 DAI, which adds up to be greater than availableYield.
- `_amountOut` is 90 DAI. yieldFeeBalance is increased by 9 DAI.
- Fee recipient doesn't collect the fees and the protocol grows until it's time to call `transferTokensOut()` again. `availableYield` is 99 DAI again, and yieldFeeBalance is increased by 9 DAI.

This happens until `yieldFeeBalance` is 99 DAI. Fee recipient decides to collect his fees. He calls `claimYieldFeeShares()` which mints the shares of the PrizeVault (which is 1:1 with DAI, assuming everything goes well).

Now, the PrizeVault has about 20 sDAI. Fee recipient cannot claim since the PrizeVault needs about 90 sDAI (assuming DAI:sDAI = 1.1:1). Alice comes in and participates in the protocol. She deposits 110 DAI and gets 110 shares. The protocol gets about 100 sDAI, for a total of 120 sDAI

The fee recipient can burn his 99 shares for DAI, and the protocol will have ~30 sDAI left. Alice cannot withdraw her whole deposit. She cannot burn her 110 shares to get back 110 DAI since the
A Malicious Prize Winner Can Steal the ClaimReward From the Claimer Using the BeforeClaimPrize Hook
Audit: 2024-03-pooltogether-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/abstract/Claimable.sol#L76-L119


# Vulnerability details

## Impact
Claimer will lose part of their profit. 
If too many winners set such a hook, this can disencentivize claimers to do their job.

## Vulnerability details
Pool Together prize distribution is based on incentivization of 3rd parties to activate its functionality, rather than depending on a governance or centralized actor. For example in this case, everytime a draw to award prizes is closed, any 3rd party can call the `claimable::claimPrize` function to claim the winner's prize on his behalf. By doing so, the prize will be distributed to the winner, minus a small fee rewarded to the claimer.

As said above, to claim prizes 3rd party must call the `Claimable::claimPrize`:

https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/abstract/Claimable.sol#L76-L119

```solidity
File: pt-v5-vault\src\abstract\Claimable.sol
076:     function claimPrize(
077:         address _winner,
078:         uint8 _tier,
079:         uint32 _prizeIndex,
080:         uint96 _reward,
081:         address _rewardRecipient
082:     ) external onlyClaimer returns (uint256) {
083:         address recipient;
084: 
085:         if (_hooks[_winner].useBeforeClaimPrize) {
086:             recipient = _hooks[_winner].implementation.beforeClaimPrize{ gas: HOOK_GAS }(
087:                 _winner,
088:                 _tier,
089:                 _prizeIndex,
090:                 _reward,
091:                 _rewardRecipient
092:             );
093:         } else {
094:             recipient = _winner;
095:         }
096: 
097:         if (recipient == address(0)) revert ClaimRecipientZeroAddress();
098: 
099:         uint256 prizeTotal = prizePool.claimPrize(
100:             _winner,
101:             _tier,
102:             _prizeIndex,
103:             recipient,
104:             _reward,
105:             _rewardRecipient
106:         );
```

And as we can see here, before the prize is claimed (L99), a hook is called (L86).
We can observe that this hook receives the exact same parameters as the `claimPrize` call: `_winner`, `_tier`, `_prizeIndex`, `reward` and `rewardRecipient`.

The vulnerability lies in the fact that nothing prevent a malicious winner to configure a hook that will claim the prize before the original claimer.
By developping a hook that reuses these parameters to call `claimable::claimPrize`, but replacing the `_rewardRecipient` address with one of its own, the winner can then receive its legitimate prize + the claimer reward.
And as a bonus, we can also note that the winner will not even have to pay the gas fee for the call, as it uses the claimer call to execute this action.

## Proof of Concept

By claiming right before the original claimer thanks to the hook, the winner sets the `_claimedPrizes[msg.sender][_w
Return value of ERC20::transfer function is not checked in withdraw
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#L939


# Vulnerability details

## Summary
Code does not handle ERC20 tokens with special transfer implementation . Calls to ERC20::transfer method should always be checked as specified in the EIP-20

## Impact
Some ERC20 tokens do not revert on failure in transfer but instead return false as a return value. Because of this it has become a common practice to use OpenZeppelin's SafeERC20 to handle such weird tokens. If transfer fails, but does not revert it can leave tokens stuck in the contract . If this becomes the case, the vault accounting might be flawed. 
The impact, potentially, is a permanently lost ( stuck) value for users of the protocol who utilizes such a weird ERC20 token as asset.  

## Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

## Tools Used

Manual review

## Recommended Mitigation Steps
Use Openzeppelin's SafeERC20 functions to handle the transfer of such tokens. Implement safeTransfer function in the SafeERC20 contract in this portion of the code.


## Assessed type

Token-Transfer
`prizeVault::withdraw()` lacks slippage checks
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#L489


# Vulnerability details


## Summary
`prizeVault::withdraw()` is used to withdraw exact number of assets by buring shares and sent to receiver. 

But this function lacks maxShares burnt param, without this caller could burn more than expected amount of shares for corresponding assets. 

## Impact


caller will end up burning more shares than he expect

## Proof of Concept


```solidity
    function withdraw(
        uint256 _assets,
        address _receiver,
        address _owner
    ) external returns (uint256) {
        uint256 _shares = previewWithdraw(_assets);
        _burnAndWithdraw(msg.sender, _receiver, _owner, _shares, _assets);
        return _shares;
    }

```
https://github.com/code-423n4/2024-03-pooltogether/blob/main/pt-v5-vault/src/PrizeVault.sol#L489 
withdraw only takes `_assets` the amount of underlying assets to be withdrawn by burning whatever amount of shares which is not an optimised trade for caller.

In many case caller could end up burning more than he expected

eg. before submitting transaction Alice see that everything is fine 
  so she submitted a tx for withdrawing assets
  due to some malicious actions or inconsistent behaviour vault goes into recovery mode i.e `totalAssets()< totalDebt()`

  Now 1 share corresponds to less than 1 asset but Alice tx will be executed as normal tx and she will end up burning more shares than she expected 

## Tools Used
Manual
## Recommended Mitigation Steps

Add proper slippage checks during withdrawing




## Assessed type

Other
`yieldFeeBalance` wouldn't be claimed after calling `transferTokensOut()`. 
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#L692


# Vulnerability details

## Impact
`yieldFeeBalance` wouldn't be claimed after calling `transferTokensOut()` due to the twab supply limit.

## Proof of Concept
When `_tokenOut == address(this)`, `liquidatableBalanceOf()` mints shares to the receiver and accumulates `yieldFeeBalance` accordingly.

But when it checks the maximum liquidatable amount in [liquidatableBalanceOf()](https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L636), it validates the twap supply limit with the [liquidYield](https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L647) only and it might meet the supply limit while minting [yieldFeeBalance](https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L619) like the below.

- We assume `totalSupply = 6e28, yieldFeeBalance = 0, twabSupplyLimit = 2^96 - 1 = 7.9e28` and the vault has enough available yield.
- Then `liquidatableBalanceOf(_tokenOut = address(this))` will return `_maxAmountOut = 7.9e28 - 6e28 = 1.9e28` when [_liquidYield > _maxAmountOut](https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L653).
- After calling `transferTokensOut()` with `_amountOut = 1.9e28`, [_yieldFee](https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L675) will be added to `yieldFeeBalance` but it can't be claimed as we met the twap supply limit already.

## Tools Used
Manual Review

## Recommended Mitigation Steps
[liquidatableBalanceOf()](https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L649) shouldn't apply `yieldFeePercentage` to compare with `_maxAmountOut` when `_tokenOut == address(this)`.

```solidity
    function liquidatableBalanceOf(address _tokenOut) public view returns (uint256) {
        uint256 _totalSupply = totalSupply();
        uint256 _maxAmountOut;
        if (_tokenOut == address(this)) {
            // Liquidation of vault shares is capped to the TWAB supply limit.
            _maxAmountOut = _twabSupplyLimit(_totalSupply);
        } else if (_tokenOut == address(_asset)) {
            // Liquidation of yield assets is capped at the max yield vault withdraw plus any latent balance.
            _maxAmountOut = _maxYieldVaultWithdraw() + _asset.balanceOf(address(this));
        } else {
            return 0;
        }

        // The liquid yield is computed by taking the available yield balance and multiplying it
        // by (1 - yieldFeePercentage), rounding down, to ensure that enough yield is left for the
        // yiel
`_twabSupplyLimit()` returns a wrong limit.
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#L800


# Vulnerability details

## Impact
The yield fee recipient couldn't claim his yield fee shares due to the twap supply limit.

## Proof of Concept
The yield fee recipient can claim already earned yield fee shares using [claimYieldFeeShares()](https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L619).

Due to the TwabController's limits, we check the available supply using [_twabSupplyLimit()](https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L800) but we don't check `yieldFeeBalance` that can be claimed any time.

So the below case would be possible.

- We assume `totalSupply = 6e28, yieldFeeBalance = 1e28, twabSupplyLimit = 2^96 - 1 = 7.9e28`.
- [maxDeposit()](https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L380) will return `twabSupplyLimit - totalSupply = 1.9e28`.
- A user calls `deposit()` with the above maximum amounts and `totalSupply = twabSupplyLimit` now.
- Then [claimYieldFeeShares()](https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L611) will revert due to the twap supply limit.

## Tools Used
Manual Review

## Recommended Mitigation Steps
`_twabSupplyLimit()` should calculate the remaining supply using `totalSupply + yieldFeeBalance` and `deposit()` should validate the limit as well.

```solidity
    function _twabSupplyLimit(uint256 _totalSupply) internal pure returns (uint256) {
        unchecked {
            return type(uint96).max - _totalSupply - yieldFeeBalance;
        }
    }
```


## Assessed type

Invalid Validation
deposit and mint function does not check for the maxDeposit and Maxmint 
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
https://github.com/code-423n4/2024-03-pooltogether/blob/main/pt-v5-vault/src/PrizeVault.sol#L843


# Vulnerability details

## Summary

deposit and mint function does not check for the maxDeposit and Maxmint.SO it permits  the deposit amount to mint shares more than the `maxDeposit` and `maxMint` amount

## Proof of Concept

```solidity
    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
        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;//@audit here we have maxDeposit
        }
    }
```
https://github.com/code-423n4/2024-03-pooltogether/blob/main/pt-v5-vault/src/PrizeVault.sol#L374

As maxDeposit, Considers the uint96 limit on total share supply in the TwabController but deposit and mint doesn't check for deposited and minted shares/asset are less than maxMint/maxDeposit

```solidity
    function deposit(uint256 _assets, address _receiver) external returns (uint256) {
        uint256 _shares = previewDeposit(_assets);
        _depositAndMint(msg.sender, _receiver, _assets, _shares);
        return _shares;
    }

    /// @inheritdoc IERC4626
    function mint(uint256 _shares, address _receiver) external returns (uint256) {
        uint256 _assets = previewMint(_shares);
        _depositAndMint(msg.sender, _receiver, _assets, _shares);
        return _assets;
    }

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

Further, it does all the state changes in depositAndMint() where there is no such checks for minting and depositing cap

```solidity
    function _depositAndMint(address _caller, address _receiver, uint256 _assets, uint256 _shares) internal {
        if (_shares == 0) revert MintZeroShares();
        if (_assets == 0) revert DepositZeroAssets();

        // If _asset is ERC777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the
        // `tokensToSend` hook. On the other hand, the `tokenReceived` hook that is triggered after the transfer
        // calls the vault which is assumed to not be malicious.
        //
        // Conclusion: we need to do the transfer before we mint so that any reentrancy would h
LACK OF VERIFICATION AFTER permit CALL IN depositWithPermit FUNCTION
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#L524


# Vulnerability details

## Impact
When interacting with non-conforming tokens (like DAI), the lack of verification after calling the `permit` function can lead to unexpected transactions reverts and Incorrect allowance being set

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

The standard `ERC20 permit` function is designed to allow transactions to be approved through signatures to enable a user-friendly way to interact with contracts without requiring a separate transaction for approval. However, this mechanism relies on the assumption that the permit function behaves according to a standard pattern.

However, certain tokens may not adhere to the `IERC20Permit` standard. For example, the DAI Stablecoin utilizes a `permit()` function that deviates from the reference implementation. The Vault contract's `depositWithPermit` function would not detect this anomaly. This could lead to a scenario where the function proceeds to attempt a deposit without the necessary allowance being set.This could lead to  failed transactions or unexpected contract states. 
```
        if (_asset.allowance(_owner, address(this)) != _assets) {
            IERC20Permit(address(_asset)).permit(_owner, address(this), _assets, _deadline, _v, _r, _s);
        }
```

This lack of verification may lead to inconsistencies and unexpected behavior when interacting with non-conforming tokens

## Tools Used
Manual Review
## Recommended Mitigation Steps
After performing a `permit` call, add checks to verify that the allowance was correctly set. 


## Assessed type

Other
Funds locked due to missing transfer check
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#L933-L936
https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L939


# Vulnerability details

## Impact

**Impact**: All of the user's funds are unretrievably locked in the `PrizeVault` contract.

A combination of issues allows for the following scenario:
1. Alice invokes [`_withdraw(receiver, assets)`](https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L925-L941) (via `burn()` or `withdraw()`).
2. The contract [computes the number of shares to redeem](https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L933-L934), via `previewWithdraw(assets)`.
3. The contract [redeems as many shares](https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L935-L936), but the ERC 4626-compliant vault returns fewer shares than expected.  
   At this point, the contract holds fewer than `assets` tokens.
4. The contract [attempts to `transfer` assets to the receiver](https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L939). This fails due to insufficient funds, but the ERC 20-compliant token does not revert (only returns `false`).
5. At this point, Alice's assets are locked in the `PrizeVault` contract. They cannot be withdrawn at a later point, because the corresponding prize vault and yield vault shares have been burned.

The exploit relies on insufficient handling of two corner cases of [ERC-20](https://eips.ethereum.org/EIPS/eip-20) and [ERC-4246](https://eips.ethereum.org/EIPS/eip-4626):

* [ERC-20](https://eips.ethereum.org/EIPS/eip-20) does not stipulate that `transfer` must throw if the message sender holds insufficient balance. Instead, returning `false` is compliant with ERC-20 and implemented by many tokens, including [BAT](https://etherscan.io/token/0x0d8775f648430679a709e98d2b0cb6250d2887ef), [cUSDC](https://etherscan.io/token/0x39aa39c021dfbae8fac545936693ac917d5e7563), [EURS](https://etherscan.io/token/0xdb25f211ab05b1c97d595516f45794528a807ad8), [HuobiToken](https://etherscan.io/token/0x6f259637dcd74c767781e37bc6133cd6a68aa161), [ZRX](https://etherscan.io/token/0xe41d2489571d322189246dafa5ebde1f4699f498) and many more.
* [ERC-4626](https://eips.ethereum.org/EIPS/eip-4626) does not stipulate that `redeem(previewWithdraw(assets))` transfers at least `assets`. In particular, [`redeem(shares, ...)`](https://eips.ethereum.org/EIPS/eip-4626#redeem) only guarantees that exactly `shares` are burned. The only guaranteed way to gain a certain amount of assets is by calling [`withdraw(assets, ...)`](https://eips.ethereum.org/EIPS/eip-4626#withdraw
depositWithPermit allowance check is incorrect 
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#L539-L541


# Vulnerability details

## Impact
Users are allowed to deposit via the `depositWithPermit` which uses user signatures to increase the allowance of the asset used in the prizeVault to complete the deposit without the user having to do more approvals than necessary.

## Proof of Concept
An issue arises in the way that the check is implemented 

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

```
if (_asset.allowance(_owner, address(this)) != _assets) {
IERC20Permit(address(_asset)).permit(_owner, address(this), _assets, _deadline, _v, _r, _s);
  }
```
As we can see above we check if the allowance is not the same as the amount of assets we want to deposit, this means that the contract can have a large allowance to spend the user's tokens already, and with the current implementation the contract will need to get even more allowance for the user to be able to deposit.

## Tools Used
Vscode
## Recommended Mitigation Steps
Consider changing to the lines shown below
```
if (_asset.allowance(_owner, address(this)) < _assets) {
IERC20Permit(address(_asset)).permit(_owner, address(this), _assets, _deadline, _v, _r, _s);
  }
```



## Assessed type

Invalid Validation