xanoutas/solidity-security-findings huggingface dataset:
When a user sponsors with a certain amount, all his previous delegate will also be cleared
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#L552-L565


# Vulnerability details

## Impact
The usage of `sponsor` function is unclear with `_assets` as input but actually clear all the delegate balance of `msg.sender`. When a user wants to calls `sponsor` to promote a campaign and attract more users to use the vault, he may only want to delegate the input amount argument in the function, but the delegate balance of the previous delegated address is also cleared.

## Proof of Concept
The usage of `sponsor` function is unclear with `_assets` as input but actually clear all the delegate balance of `msg.sender`.

In the [sponsor](https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L552-L565), if a user calls `sponsor` to deposit `_assets` of underlying tokens and delegate to `SPONSORSHIP_ADDRESS`, he may only want to delegate the input amount argument `_assets` in the function to `SPONSORSHIP_ADDRESS` to promote a campaign and attract more users to use the vault.

```solidity
    function sponsor(uint256 _assets) external returns (uint256) {
        address _owner = msg.sender;

        uint256 _shares = previewDeposit(_assets);
        _depositAndMint(_owner, _owner, _assets, _shares);

        if (twabController.delegateOf(address(this), _owner) != SPONSORSHIP_ADDRESS) {
            twabController.sponsor(_owner);
        }

        emit Sponsor(_owner, _assets, _shares);

        return _shares;
    }
```

However, the delegate balance of the previous delegated address is also cleared, causing unintended behavior.

The PoC is shown below (in ``PrizeVault.t.sol):
```

    function testSponsor_clear() public {
        underlyingAsset.mint(address(this), 100);
        underlyingAsset.approve(address(vault), 100);
        vault.deposit(100, address(this));
        // sponsor the vault
        twabController.delegate(address(vault),address(2));
        uint256 assets = 4e18;
        underlyingAsset.mint(address(this), assets);
        underlyingAsset.approve(address(vault), assets);
        vm.expectEmit();
        emit Sponsor(address(this), assets, assets); // 1:1
        uint256 shares = vault.sponsor(assets);
        emit log_named_uint("delegate",twabController.delegateBalanceOf(address(vault),address(2)));
        // twabController.delegate(address(vault),address(2));
        // emit log_named_uint("delegate",twabController.delegateBalanceOf(address(vault),address(2)));

    }
```

The result
```
[PASS] testSponsor_clear() (gas: 504472)
Logs:
  delegate: 0
```

## Tools Used

Foundry

## Recommended Mitigation Steps

It's would be proper to separate the function to avoid confusion or clearly demonstrate the result in the doc or comments.


## Assessed type

Invalid Validation
YieldFee in PrizeVault is calculated incorrectly
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#L671-L676


# Vulnerability details

## Impact

`transferTokensOut()` will not work and revert if `yieldFee` is too large. 

## Proof of Concept

When collecting rewards, the LiquidationPair will call `transferTokensOut`. `_amountOut` will be the asset of the PrizeVault.

Assume sDAI is the underlying yield contract. `_amountOut` will be DAI. Assume that there is enough yield to transfer tokens out.

This is how the current YieldFee is calculated. 

```
        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;
        }
```

Focusing on this line: 
```
            _yieldFee = (_amountOut * FEE_PRECISION) / (FEE_PRECISION - _yieldFeePercentage) - _amountOut;
```

- FEE_PRECISION is 1e9
- _yieldFeePercentage is 9e8 (90%)
- `_amountOut` is 100e18 DAI

If the amountOut is 100e18 DAI, and the yieldFeePercentage is at 90%, that means that the yieldFee should be 90 DAI.

```
            _yieldFee = (_amountOut * FEE_PRECISION) / (FEE_PRECISION - _yieldFeePercentage) - _amountOut;

           _yieldFee = (100e18 * 1e9) / (1e9 - 9e8) - 100e18;
           _yieldFee = (100e27) / (1e8) - 100e18;
           _yieldFee = 100e19 - 100e18;
           _yieldFee = 900e18

```
`yieldFee` becomes 900 DAI, instead of 90 DAI.

As a result, the following lines, 

```
if (_amountOut + _yieldFee > _availableYield) {
            revert LiquidationExceedsAvailable(_amountOut + _yieldFee, _availableYield);
        }
```

will revert as if the `availableYield` is 500 DAI, then 100 + 900 DAI = 1000 DAI.

## Tools Used

Manual Review

## Recommended Mitigation Steps

Recalculate the `_yieldFee`. Multiply `_amountOut` by `_yieldFeePercentage`, then divide it by `FEE_PRECISION`.

```
+           _yieldFee = (_amountOut * _yieldFeePercentage) / FEE_PRECISION;
```


## Assessed type

Decimal
A potential vulnerability of  yieldFeeBalance not being claimed by anyone, effectively locking the yield fee shares forever.
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#L311
https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L122
https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L958-L961
https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L268-L271
https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L611


# Vulnerability details

## Impact
It could potentially lead to a situation where the `yieldFeeBalance` (accrued yield fee shares) cannot be claimed by anyone, effectively locking those yield fee shares forever. This is because the `claimYieldFeeShares` function has a modifier `onlyYieldFeeRecipient` that only allows the `yieldFeeRecipient` address to call it. If the `yieldFeeRecipient` is set to the zero address (which is an invalid Ethereum address), no one can call the `claimYieldFeeShares` function, effectively locking the yield fee shares permanently.
The severity of this issue is High. Locking yield fee shares permanently is a significant issue as it causes a permanent loss of funds (the yield fee shares) for the intended recipient. This could lead to financial losses .

## Proof of Concept
The provided code does not check if the `yieldFeeRecipient` is set to the zero address during construction or when updating it using the `_setYieldFeeRecipient` function. This could lead to a situation where the `yieldFeeBalance` cannot be claimed by anyone, effectively locking the yield fee shares forever.
Explanation:
1. During the contract construction, the `yieldFeeRecipient` is set without any validation:

       constructor(
           // ...
           address yieldFeeRecipient_,
           // ...
       ) {
           // ...
           _setYieldFeeRecipient(yieldFeeRecipient_);
           // ...
       }
2. The `_setYieldFeeRecipient` function does not validate if the provided address is non-zero:

       function _setYieldFeeRecipient(address _yieldFeeRecipient) internal {
           yieldFeeRecipient = _yieldFeeRecipient;
           emit YieldFeeRecipientSet(_yieldFeeRecipient);
       }
3. The `claimYieldFeeShares` function, which is responsible for transferring the `yieldFeeBalance` to the `yieldFeeRecipient`, has a modifier `onlyYieldFeeRecipient` that checks if the caller is the `yieldFeeRecipient`:

       modifier onlyYieldFeeRecipient() {
           if (msg.sender != yieldFeeRecipient) {
               revert CallerNotYieldFeeRecipient(msg.sender, yieldFeeRecipient);
           }
           _;
       }

       function claimYieldFeeShares(uint256 _shares) external 
       onlyYieldFeeRecipient {
           // ...
       }

If the `yieldFeeRecipient` is set
No checking of maxWithdraw and maxRedeem in `withdraw()` and `redeem()` can lead loss of asset funds in PrizeVault.sol
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#L938-L940
https://github.com/code-423n4/2024-03-pooltogether/blob/main/pt-v5-vault/src/PrizeVault.sol#L489
https://github.com/code-423n4/2024-03-pooltogether/blob/main/pt-v5-vault/src/PrizeVault.sol#L500
https://github.com/code-423n4/2024-03-pooltogether/blob/main/pt-v5-vault/src/PrizeVault.sol#L887
https://github.com/code-423n4/2024-03-pooltogether/blob/main/pt-v5-vault/src/PrizeVault.sol#L907


# Vulnerability details

## Impact
No checking of maxWithdraw and maxRedeem in `withdraw()` and `redeem()` can lead loss of asset funds in PrizeVault.sol

## Proof of Concept
The function `withdraw()` and `redeem()` don't check about maxWithdraw and maxRedeem.
```
    function withdraw(
        uint256 _assets,
        address _receiver,
        address _owner
    ) external returns (uint256) { //@audit-issue user can withdraw more than maxWithdraw
        uint256 _shares = previewWithdraw(_assets);
        _burnAndWithdraw(msg.sender, _receiver, _owner, _shares, _assets);
        return _shares;
    }
```
```
    function redeem(
        uint256 _shares,
        address _receiver,
        address _owner
    ) external returns (uint256) { //@audit-issue user can redeem more than maxRedeem
        uint256 _assets = previewRedeem(_shares);
        _burnAndWithdraw(msg.sender, _receiver, _owner, _shares, _assets);
        return _assets;
    }
```

Two functions are calling `_burnAndWithdraw`, which calls `_withdraw`. `_withdraw` function is as following.
```
    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);
        }
    }
```
So if there are enough amounts of asset in PrizeVault contract (_assets <= _latentAssets), malicious user can withdraw more than amount relevant to his shares.
There is possiblity that enough amounts are accured on PrizeVault contract by `withdraw` or `transferTokensOut`, which do not confirm that _receiver can't be prize vault contract.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Need to check that withdraw amount is less than maxWithdraw in `withdraw` function, 
and redeem amount is less than maxRedeem in `redeem` function


## Ass
`PrizeVault.sol::claimYieldFeeShares` will not work as expected if `_shares < yieldFeeBalance`
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-L622


# Vulnerability details

## Impact
`PrizeVault.sol::claimYieldFeeShares` will not work as expected if `_shares < yieldFeeBalance`

## Proof of Concept
`claimYieldFeeShares` is as following.
```
    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);
    }
```
yieldFeeBalance is subtracted by _yieldFeeBalance, which is same as yieldFeeBalance.
As a result, if _shares < yieldFeeBalance, vault contract knows as if all remaining yieldFeeBalance is minted to fee recipient, but in reality, just _shares minted.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Replace `yieldFeeBalance -= _yieldFeeBalance;' with `yieldFeeBalance -= _shares;`


## Assessed type

Error
A Prizevault share holder can steal funds of other users In edge case
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#L433


# Vulnerability details

## Impact
In the PoolToogether documentation It was explicitly stated that " if the underlying yield source loses assets, depositors will only be able to withdraw a proportional amount of remaining assets based on their share balance and the total debt balance." However the implementation in the `Prizevault` doesnt fully implements this, it allows for a user/depositor in a situation where there is loss of asset to withdraw their inital deposit at the expense of other users.  Lets do a walk through of the codebase;  
```
else {
                // Convert to shares while rounding up. Since 1 asset is guaranteed to be worth more than
                // 1 share and any upwards rounding will not exceed 1 share, we can be sure that when the
                // shares are converted back to assets (rounding down) the resulting asset value won't
                // exceed `_maxWithdraw`.
                uint256 _maxScaledRedeem = _maxWithdraw.mulDiv(totalDebt_, _totalAssets, Math.Rounding.Up);
                return _maxScaledRedeem >= _ownerShares ? _ownerShares : _maxScaledRedeem;

```

When underlying are lost from yieldvault means `_totalAssets < totalDebt_` which causes the block of code above to be executed.
`uint256 _maxScaledRedeem = _maxWithdraw.mulDiv(totalDebt_, _totalAssets, Math.Rounding.Up)`
This calculates the maximum redeemable share of the whole price vault token based on the available underlying asset in the yeildvault. There is no calculation to take account of the proportional maximum prizevault share each user can redeem. 

`return _maxScaledRedeem >= _ownerShares ? _ownerShares : _maxScaledRedeem;`
As we can see here the return value checks if `_ownerShares` which is the total share of the owner is greater than maximum redeamable share of the prize vault and if it is,that user gets to withdraw all their initial deposit at expense of all other users because proportionality based on each users share of the prizevault wasnt accounted for in `maxredeem` and `withdraw` function. If a user's share of the prize vault according to the implementation is greater than all of the of the `_maxScaledRedeem` they get all the the underlying asset in the vault and all other users get nothing.

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

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

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

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

```
function maxRedeem(address _owner) p
`yieldFeeBalance` is incorrectly reduced when claiming yield fee shares.
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#L617


# Vulnerability details

## Impact
Yield fee shares are incorrectly reduced when claiming part of them, resulting that the remaining yield fee shares will not be able to be claimed again.

## Proof of Concept
At line 617, the `yieldFeeBalance` is reduced by `_yieldFeeBalance`. However, the real claimed amount is `_shares`. It is possible that `_shares < _yieldFeeBalance`, in which case, part of the yield fee shares will be locked in the vault and will not be able to be claimed.
```solidity
611:    function claimYieldFeeShares(uint256 _shares) external onlyYieldFeeRecipient {
612:        if (_shares == 0) revert MintZeroShares();
613:
614:        uint256 _yieldFeeBalance = yieldFeeBalance;
615:        if (_shares > _yieldFeeBalance) revert SharesExceedsYieldFeeBalance(_shares, _yieldFeeBalance);
616:
617:=>      yieldFeeBalance -= _yieldFeeBalance;
618:
619:        _mint(msg.sender, _shares);
620:
621:        emit ClaimYieldFeeShares(msg.sender, _shares);
622:    }
```
https://github.com/code-423n4/2024-03-pooltogether/blob/main/pt-v5-vault/src/PrizeVault.sol#L611-L622

## Tools Used
VSCode

## Recommended Mitigation Steps
After claiming, decrease `yieldFeeBalance` by `_shares` instead of `_yieldFeeBalance`.
```solidity
    function claimYieldFeeShares(uint256 _shares) external onlyYieldFeeRecipient {
        if (_shares == 0) revert MintZeroShares();

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

-        yieldFeeBalance -= _yieldFeeBalance;
+        yieldFeeBalance -= _shares;

        _mint(msg.sender, _shares);

        emit ClaimYieldFeeShares(msg.sender, _shares);
    }
```


## Assessed type

Math
user can with draw less funds than minted and deposited
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#L476
https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L441
https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L543


# Vulnerability details

## Impact
user can loss funds when they redeem than when they use withdraw
user can sometimes withdraw more that they deposited if someone deposits more 
because the protocol uses `balanceof`
 
## Proof of Concept
 this function returns the same assert you give it
```solidity
        function previewDeposit(uint256 _assets) public pure returns (uint256) {
           return _assets;
    }
```

when you redem is not a 1:1 method used but an assert is converted to shares
```soliidity
   function previewRedeem(uint256 _shares) public view returns (uint256) {
        return convertToAssets(_shares);
    }

     function convertToAssets(uint256 _shares) public view returns (uint256) {
        uint256 totalDebt_ = totalDebt();
        uint256 _totalAssets = totalAssets();
        if (_totalAssets >= totalDebt_) {
            return _shares;
        } else {
            
            return _shares.mulDiv(_totalAssets, totalDebt_, Math.Rounding.Down);
        }
    }

    function maxRedeem(address _owner) public view returns (uint256) {
        uint256 _maxWithdraw = _maxYieldVaultWithdraw() + _asset.balanceOf(address(this));
        uint256 _ownerShares = balanceOf(_owner);

         
       
        if (_ownerShares > _maxWithdraw) {
            uint256 _totalAssets = totalAssets();
            uint256 totalDebt_ = totalDebt();
            if (_totalAssets >= totalDebt_) {
                return _maxWithdraw;
            } else {
                
                 
                uint256 _maxScaledRedeem = _maxWithdraw.mulDiv(totalDebt_, _totalAssets, Math.Rounding.Up);
                return _maxScaledRedeem >= _ownerShares ? _ownerShares : _maxScaledRedeem;
            }
        } else {
            return _ownerShares;
        }
    }
     function redeem(
        uint256 _shares,
        address _receiver,
        address _owner
    ) external returns (uint256) {
        uint256 _assets = previewRedeem(_shares);
        _burnAndWithdraw(msg.sender, _receiver, _owner, _shares, _assets);
        return _assets;
    }


```

## Tools Used
manual , slither

## Recommended Mitigation Steps
have a mapping to track user deposits before they redeem funds
use the same method maths logic in revers for redeem that was used for deposits


## Assessed type

Math
`PrizeVault::maxRedeem` does incorrect comparison
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#L422


# Vulnerability details

## Summary
`PrizeVault::maxRedeem` calculates max number of shares can be redeeem . 
In the computation it involves incorrect comparison which may give wrong results

## Proof of Concept

```solidity
    function maxRedeem(address _owner) public view returns (uint256) {
        uint256 _maxWithdraw = _maxYieldVaultWithdraw() + _asset.balanceOf(address(this));
        uint256 _ownerShares = balanceOf(_owner);

        // The owner will never receive more than 1 asset per share, so there is no need to convert max
        // withdraw to shares unless the owner has more shares than the max withdraw and is redeeming
        // at a loss (when 1 share is worth less than 1 asset).
        if (_ownerShares > _maxWithdraw) {
            uint256 _totalAssets = totalAssets();
            uint256 totalDebt_ = totalDebt();
            if (_totalAssets >= totalDebt_) {
                return _maxWithdraw;
            } else {
                // Convert to shares while rounding up. Since 1 asset is guaranteed to be worth more than
                // 1 share and any upwards rounding will not exceed 1 share, we can be sure that when the
                // shares are converted back to assets (rounding down) the resulting asset value won't
                // exceed `_maxWithdraw`.
                uint256 _maxScaledRedeem = _maxWithdraw.mulDiv(totalDebt_, _totalAssets, Math.Rounding.Up);
                return _maxScaledRedeem >= _ownerShares ? _ownerShares : _maxScaledRedeem;
            }
        } else {
            return _ownerShares;
        }
    }
```
https://github.com/code-423n4/2024-03-pooltogether/blob/main/pt-v5-vault/src/PrizeVault.sol#L422

1. calculates the `_maxWithdraw` assets including max withdrawable assets from yield vault and remaining dust in prizeVault contract

2. calculates the `_ownerShares` shares of the owner

and then it compare assets value to share value for further computation. which is incorrect since in recovery mode assets and shares won't be 1:!

In the recovery mode  `totalAssets()< totalDebt()`
and  Owner may receive more than 1 share for 1 asset in this case

i.e converting assets in shares will be greater value than assets value [Magnitude wise]

it cleary states that `_ownerShares > _maxWithdraw` won't be always correct and  leads to incorrect computation further



## Tools Used
Manual
## Recommended Mitigation Steps

During comparison changes assets to shares or shares to assets 
i.e unit should be same during comparisons


## Assessed type

ERC4626