xanoutas/solidity-security-findings huggingface dataset:
Depositors might lose funds due to the lack of zero share check
Audit: 2023-07-pooltogether-findings
Severity: MEDIUM

# Lines of code

https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L407-L413


# Vulnerability details

## Impact

Depositors might lose funds due to the lack of checking whether the shares to be minted is equal to zero. When this happens, the assets will be deposited into the vault, but the depositors will receive zero shares. This is independent from the initial depositor attack issue, which means, even the first depositor attack issue is resolved, the issue here still needs to be addressed.

## Proof of Concept

Suppose the exchange rate is 1.1e18. Alice calls deposit() with assets = 1.

```
  function deposit(uint256 _assets, address _receiver) public virtual override returns (uint256) {
    if (_assets > maxDeposit(_receiver))
      revert DepositMoreThanMax(_receiver, _assets, maxDeposit(_receiver));


    uint256 _shares = _convertToShares(_assets, Math.Rounding.Down);
    _deposit(msg.sender, _receiver, _assets, _shares);


    return _shares;
  }
```

The number of shares calculated will be 1 * 1e18 / 1.1e18 = 0

```
  function _convertToShares(
    uint256 _assets,
    Math.Rounding _rounding
  ) internal view virtual override returns (uint256) {
    uint256 _exchangeRate = _currentExchangeRate();


    return
      (_assets == 0 || _exchangeRate == 0)
        ? _assets
        : _assets.mulDiv(_assetUnit, _exchangeRate, _rounding);
  }
```

Normally, when shares = 0; the deposit() function should revert, but in this case it does not. As a result, the depositor will lose 1 asset tokens and return zero shares.


## Tools Used

Manual Review

## Recommended Mitigation Steps

It is critical to have a zero share check and revert when the number of shares is zero.

```
  function deposit(uint256 _assets, address _receiver) public virtual override returns (uint256) {
    if (_assets > maxDeposit(_receiver))
      revert DepositMoreThanMax(_receiver, _assets, maxDeposit(_receiver));


    uint256 _shares = _convertToShares(_assets, Math.Rounding.Down);

+   if(_shares == 0) revert ZeroSharesDeposit();
    _deposit(msg.sender, _receiver, _assets, _shares);


    return _shares;
  }
```


## Assessed type

ERC4626
`isTimeSafe` and `isTimeRangeSafe` not implemented in the functions `getBalanceAt` and `getTwabBetween`
Audit: 2023-07-pooltogether-findings
Severity: MEDIUM

# Lines of code

https://github.com/GenerationSoftware/pt-v5-twab-controller/blob/0145eeac23301ee5338c659422dd6d69234f5d50/src/libraries/TwabLib.sol#L278-L321
https://github.com/GenerationSoftware/pt-v5-twab-controller/blob/0145eeac23301ee5338c659422dd6d69234f5d50/src/libraries/TwabLib.sol#L254-L276


# Vulnerability details


## Impact

The Natspec of both `getBalanceAt` and `getTwabBetween` functions indicates that they should implement the functions `isTimeSafe` and `isTimeRangeSafe` (respectively) to ensure that the queried timestamps are safe, but both functions don't implement them which can lead both functions to return wrong values which in the end will impact the protocol accounting as those function are used to fetch the users balances fro a given period.

## Proof of Concept

I will start by explaining what the functions `isTimeSafe` and `isTimeRangeSafe` actually do. The `isTimeRangeSafe` is basically a double call to the function `isTimeSafe` for the start and end timestamps provided to it, so to understand what both functions are doing we should look at `isTimeSafe` :

```solidity
function _isTimeSafe(
    uint32 PERIOD_LENGTH,
    uint32 PERIOD_OFFSET,
    ObservationLib.Observation[MAX_CARDINALITY] storage _observations,
    AccountDetails memory _accountDetails,
    uint32 _time
) private view returns (bool) {
    // @audit 1- check when there is no observations
    // If there are no observations, it's an unsafe range
    if (_accountDetails.cardinality == 0) {
      return false;
    }
    // If there is one observation, compare it's timestamp
    uint32 period = _getTimestampPeriod(PERIOD_LENGTH, PERIOD_OFFSET, _time);
    
    // @audit 1- check when there is a single observation
    if (_accountDetails.cardinality == 1) {
      return
        period != _getTimestampPeriod(PERIOD_LENGTH, PERIOD_OFFSET, _observations[0].timestamp)
          ? true
          : _time >= _observations[0].timestamp;
    }
    ObservationLib.Observation memory preOrAtObservation;
    ObservationLib.Observation memory nextOrNewestObservation;
    
    // @audit 2- check on the newest observation
    (, nextOrNewestObservation) = getNewestObservation(_observations, _accountDetails);
    
    if (_time >= nextOrNewestObservation.timestamp) {
      return true;
    }
    
    // @audit 3- check on the period of the oberservations after & before
    (preOrAtObservation, nextOrNewestObservation) = _getSurroundingOrAtObservations(
      PERIOD_OFFSET,
      _observations,
      _accountDetails,
      _time
    );
    
    uint32 preOrAtPeriod = _getTimestampPeriod(
      PERIOD_LENGTH,
      PERIOD_OFFSET,
      preOrAtObservation.timestamp
    );
    uint32 postPeriod = _getTimestampPeriod(
      PERIOD_LENGTH,
      PERIOD_OFFSET,
      nextOrNewestObservation.timestamp
    );
    
    // The observation after it falls in a new period
    return period >= preOrAtPeriod && period < postPeriod;
}
```

The functions working can be resumed into performing 3
deposit function does not check for the `maxMint` amount.
Audit: 2023-07-pooltogether-findings
Severity: MEDIUM

# Lines of code

https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L407-L415


# Vulnerability details

## Impact
It is theoretically possible for the deposit amount to mint shares more than the `maxMint` amount

## Proof of Concept
The deposit function has a check for `maxDeposit` and reverts if the deposit value is more than max(uint96). But, it does not check the shares to be less than `maxMint` amount and hence bypasses this check. Theoretically, if the assets are equal to max(uint96) and if the vault is under-collateralised, the ratio of `_assetUnit` to `_exchangeRate` is greater than or equal to 2, then the calculation in `_convertToShares`: 

`_assets.mulDiv(_assetUnit, _exchangeRate, _rounding);` could return a value more than the `maxMint` amount. This is possible in those scenarios where the `_assetUnit` is a big enough number (possible, as there is no limit on the decimals of the underlying asset, and `_assetUnit = 10 ** super.decimals()`) and the Vault is severely under-collateralized.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Include the maxMint check in the deposit function to prevent this problem.


## Assessed type

Invalid Validation
Possible centralization issue in PrizePool.closeDraw
Audit: 2023-07-pooltogether-findings
Severity: MEDIUM

# Lines of code

https://github.com/GenerationSoftware/pt-v5-prize-pool/blob/main/src/PrizePool.sol#L348


# Vulnerability details

The winning random number is chosen by DrawManager, which will lead to centralization risk. Despite havent`t really deep dive in codebase of this issue, but if DrawManager ,can somehow calculate which random number can make their controlled address is winner with bigest prize, it will be catastrophic

## Impact
Centralization issue.

## Tools Used
Manual review.

## Recommended Mitigation Steps
Using ChainlinkVRF to generate fair random number for draw.


## Assessed type

Other
Missing External Transfer Function In Vault
Audit: 2023-07-pooltogether-findings
Severity: MEDIUM

# Lines of code

https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L1154


# Vulnerability details

## Impact

Balances of TwabController for a vault can not be transferred.

## Proof of Concept

The Vault implements an internal `_transfer` function meant to be used to transfer balances within the `TwabController`:

```js
  function _transfer(address _from, address _to, uint256 _shares) internal virtual override {
    _twabController.transfer(_from, _to, uint96(_shares));

    emit Transfer(_from, _to, _shares);
  }
```

However, there is no externally callable function implemented that makes use of this function.

## Tools Used

Manual Review

## Recommended Mitigation Steps

Add an external function that makes use of `transfer`, while also performing necessary input validation if necessary


## Assessed type

Other
Adding balance to accumulator does not depend on the current drawId, while documentation says it does
Audit: 2023-07-pooltogether-findings
Severity: MEDIUM

# Lines of code

https://github.com/GenerationSoftware/pt-v5-prize-pool/blob/4bc8a12b857856828c018510b5500d722b79ca3a/src/libraries/DrawAccumulatorLib.sol#L64-L112


# Vulnerability details

## Impact
In documentation protocol states that :
To compute the allocated contribution for a draw `d` we'd compute the integral of curve `c(d)=−t∗ln(α)∗α^d` from lastdraw d(old) to d(new), and which is equal to 
`−t∗ α^d(old) + t∗ α^d(new)`. Which clearly shows that contribution only does not depend the interval of lastdraw to new draw while it also depends the current draw.

Now Coming to actual implementation , the implemented code does not depend on the current drawId, it only depends on the interval between new draw and completed draw.

if vaults are not producing high yields and new contributions are not good enough This calculation can break protocol and will consume reserve continuously as it will calculate balance based on the interval between draws[most probably high area region of contribution curve].

## Proof of Concept
we have 2 identical `accumulator` , `accumulator1 & accumulator2`. Both have no balance in starting.
Now suppose after different scenarios of contribution both accumulators have same available balance i.e after x1 draw in accumulator1 and x2 draw in accumulator2 both have same available  balance and now for next draw both accumulators have same interval gap [newdraw-lastcompletedDraw]


```solidity
File: src/libraries/DrawAccumulatorLib.sol

function add(
    Accumulator storage accumulator,
    uint256 _amount,
    uint16 _drawId,
    SD59x18 _alpha
  ) internal returns (bool) {
    if (_drawId == 0) {
      revert AddToDrawZero();
    }
    RingBufferInfo memory ringBufferInfo = accumulator.ringBufferInfo;

    uint256 newestIndex = RingBufferLib.newestIndex(ringBufferInfo.nextIndex, MAX_CARDINALITY);
    uint16 newestDrawId_ = accumulator.drawRingBuffer[newestIndex];

    if (_drawId < newestDrawId_) {
      revert DrawClosed(_drawId, newestDrawId_);
    }

    Observation memory newestObservation_ = accumulator.observations[newestDrawId_];
    if (_drawId != newestDrawId_) {
      uint256 relativeDraw = _drawId - newestDrawId_;

      uint256 remainingAmount = integrateInf(_alpha, relativeDraw, newestObservation_.available);
      uint256 disbursedAmount = integrate(_alpha, 0, relativeDraw, newestObservation_.available);
      uint256 remainder = newestObservation_.available - (remainingAmount + disbursedAmount);

      accumulator.drawRingBuffer[ringBufferInfo.nextIndex] = _drawId;
      accumulator.observations[_drawId] = Observation({
        available: uint96(_amount + remainingAmount),
        disbursed: uint168(newestObservation_.disbursed + disbursedAmount + remainder)
      });
      uint16 nextIndex = uint16(RingBufferLib.nextIndex(ringBufferInfo.nextIndex, MAX_CARDINALITY));
      uint16 cardinality = ringBufferInfo.cardinality;
      if (ringBufferInfo.cardinality < MAX_CARDINALITY) {
        cardinality += 1;
Balance invariant between individual and total twabs can be broken
Audit: 2023-07-pooltogether-findings
Severity: MEDIUM

# Lines of code

https://github.com/GenerationSoftware/pt-v5-twab-controller/blob/0145eeac23301ee5338c659422dd6d69234f5d50/src/TwabController.sol#L554-L570


# Vulnerability details

## Impact

An edge case in the `TwabController._transferBalance` can cause total balance for a vault account to decrease although it did not actually decrease. This will cause the sum of individual delegateBalances for a vault to be greater than the registered total for that vault. This again will skew the odds in favor of winning a price, causing the reserve to be drained faster over time than intended.

## Proof of Concept

The `TwabController._transferBalance` function handles the case incorrectly where `_to` is equal to the `SPONSORSHIP_ADDRESS`. First, assume the `_from` address is an address that has a balance and delegates to itself (the default). Then the function will decrease the balance and delegateBalance of `_from`. It will also decrease the total balances of the vault account (note that `_toDelegate` will be `SPONSORSHIP_ADDRESS` due to default):

```js
    if (_from != address(0)) {
      bool _isFromDelegate = _fromDelegate == _from;

      _decreaseBalances(_vault, _from, _amount, _isFromDelegate ? _amount : 0);

      if (!_isFromDelegate && _fromDelegate != SPONSORSHIP_ADDRESS) {
        _decreaseBalances(_vault, _fromDelegate, 0, _amount);
      }

      if (
        _to == address(0) ||
        (_toDelegate == SPONSORSHIP_ADDRESS && _fromDelegate != SPONSORSHIP_ADDRESS)
      ) {
        _decreaseTotalSupplyBalances(
          _vault,
          _to == address(0) ? _amount : 0,
          (_to == address(0) && _fromDelegate != SPONSORSHIP_ADDRESS) ||
            (_toDelegate == SPONSORSHIP_ADDRESS && _fromDelegate != SPONSORSHIP_ADDRESS)
            ? _amount
            : 0
        );
      }
    }
```

Then, the balance and delegateBalance of `SPONSORSHIP_ADDRESS` will be increased by the same amount. This is not supposed to happen in the first place, as this address is not meant to have any balances. However, the issue described under [#impact](#impact) is due to the fact that the total balances of the vault account will not be adjusted, because the condition `_toDelegate != SPONSORSHIP_ADDRESS` is not met:

```js
    if (_to != address(0)) {
      bool _isToDelegate = _toDelegate == _to;

      _increaseBalances(_vault, _to, _amount, _isToDelegate ? _amount : 0);

      if (!_isToDelegate && _toDelegate != SPONSORSHIP_ADDRESS) {
        _increaseBalances(_vault, _toDelegate, 0, _amount);
      }

      if (
        _from == address(0) ||
        (_fromDelegate == SPONSORSHIP_ADDRESS && _toDelegate != SPONSORSHIP_ADDRESS)
      ) {
        _increaseTotalSupplyBalances(
          _vault,
          _from == address(0) ? _amount : 0,
          (_from == address(0) && _toDelegate != SPONSORSHIP_ADDRESS) ||
            (_fromDelegate == SPONSORSHIP_ADDRESS && _toDelegate != SPONSORSHIP_ADDRESS)
            ? _amount
            : 0
        );
      }
balanceOf method can be manipulate to liquidated vault
Audit: 2023-07-pooltogether-findings
Severity: MEDIUM

# Lines of code

https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L578


# Vulnerability details

## Impact
Deriving price from balanceOf can be manipulated to liquidate vault see example https://solodit.xyz/issues/deriving-price-with-balanceof-is-dangerous-spearbit-connext-pdf
Attacker can provide ERC20 token to the vaultAsset and mint vault shares. The deposited tokens will then be withdrawn with having same shares in the vault.
The shares then will be used to liquidate the vault and causing loss of funds for other depositors.


## Proof of Concept
https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L578

## Tools Used
Manual Review/ previous audit findings.

## Recommended Mitigation Steps
The method in this regard is based on interlinking the assets provided with vault share with function of burning vault shares when asset provided is withdrawn.



## Assessed type

Other
_getNextObservationIndex() Random use of timestamp to determine the currentTime can be manipulated bacause of dangerous strict equalities
Audit: 2023-07-pooltogether-findings
Severity: MEDIUM

# Lines of code

https://github.com/GenerationSoftware/pt-v5-twab-controller/blob/0145eeac23301ee5338c659422dd6d69234f5d50/src/libraries/TwabLib.sol#L367
https://github.com/GenerationSoftware/pt-v5-twab-controller/blob/0145eeac23301ee5338c659422dd6d69234f5d50/src/libraries/TwabLib.sol#L381
https://github.com/GenerationSoftware/pt-v5-twab-controller/blob/0145eeac23301ee5338c659422dd6d69234f5d50/src/libraries/TwabLib.sol#L529


# Vulnerability details

## Impact
The use of strict equalities can be easily manipulated by an attacker.
Miners may attempt to manipulate the timestamp.


## Proof of Concept

File: TwabLib.sol
Code Link: https://github.com/GenerationSoftware/pt-v5-twab-controller/blob/0145eeac23301ee5338c659422dd6d69234f5d50/src/libraries/TwabLib.sol#L367
Code:
   if (newestObservation.timestamp == currentTime) {


File: TwabLib.sol
Code Link: https://github.com/GenerationSoftware/pt-v5-twab-controller/blob/0145eeac23301ee5338c659422dd6d69234f5d50/src/libraries/TwabLib.sol#L381
Code:
   if (currentPeriod == 0 || currentPeriod > newestObservationPeriod) {


File: TwabLib.sol
Code Link: https://github.com/GenerationSoftware/pt-v5-twab-controller/blob/0145eeac23301ee5338c659422dd6d69234f5d50/src/libraries/TwabLib.sol#L529
Code:
   if (afterOrAtObservation.timestamp == _targetTime) {


## Tools Used

Manual Review

## Recommended Mitigation Steps

Don't use strict equality to determine the timestamp currenttime. instead, use Multiple Time Sources not relying solely on block.timestamp, consider using multiple time sources or external oracles to obtain a more reliable and tamper-resistant timestamp. 
Try to Implement time window validation to account for slight variations in the timestamp. Instead of strict equality checks, consider using ranges or thresholds to accommodate small discrepancies in the timestamps.
Also, leverage external consensus mechanisms, such as timestamping services or decentralized timestamping protocols, to obtain timestamps that are more resistant to manipulation by individual miners.
Finally, make the contract's behavior dependent on the gas limit instead of the timestamp. Since miners cannot directly manipulate the gas limit, this can provide a more reliable measure for time-dependent operations.



## Assessed type

Timing
Malicious yield vault owners can manipulate the interaction between the vault and yield vault
Audit: 2023-07-pooltogether-findings
Severity: MEDIUM

# Lines of code

https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L959


# Vulnerability details

## Impact

Malicious yield vault owners can steal depositor's assets.

## Proof of Concept

yieldVault_ is the Address of the ERC4626 vault in which assets are deposited to generate yield. After tokens are deposited into the vault, the deposit function of yieldVault is called. Since anyone can create a yieldVault and anyone can create a vault, the deposit function called may not work as the intended ERC4626 function. If the yield vault creator is malicious, the deposit function can have code that transfers all assets from the depositor to the owner instead of minting shares.

```
      SafeERC20.safeTransferFrom(
        _asset,
        _caller,
        address(this),
        _assetsDeposit != 0 ? _assetsDeposit : _assets
      );
    }


    _yieldVault.deposit(_assets, address(this));
    _mint(_receiver, _shares);
```

The deposit function of the yieldVault can take the deposited assets from the user and route it to the malicious owner without minting any shares.

## Tools Used

Manual Review

## Recommended Mitigation Steps

Make sure that all yield vaults can be trusted. Have a whitelist to check that yield vault owners that intends to create the pool together vault has a proper vault set up first.


## Assessed type

ERC4626
CONFIGURING ALLOWANCE TO ZERO FOR THE `_previousLiquidationPair` CAN BE FRONT RUN
Audit: 2023-07-pooltogether-findings
Severity: MEDIUM

# Lines of code

https://github.com/GenerationSoftware/pt-v5-prize-pool/blob/4bc8a12b857856828c018510b5500d722b79ca3a/src/PrizePool.sol#L673-L675


# Vulnerability details

## Impact
The `Vault.setLiquidationPair` is used to change the `liquidationPair_` address to a new address. In the process it sets the allowance of the `_previousLiquidationPair` to zero as shown below:

    if (_previousLiquidationPair != address(0)) {
      _asset.safeApprove(_previousLiquidationPair, 0); //@audit-info - set the allowance to zero
    } 

But this `Vault.setLiquidationPair` can be front run by the `Vault.liquidate` transaction so that the `_previousLiquidationPair` contract can liquidate the yield before the new `liquidationPair_` contract is set.

If the existing `yield` is to be used for different purpose by the new `liquidationPair_` contract, it will not happen since the existing `yield` can be liquidated by the `_previousLiquidationPair` against will of the contract owner, before new `liquidationPair_` contract is set.

## Proof of Concept

```solidity
    if (_previousLiquidationPair != address(0)) {
      _asset.safeApprove(_previousLiquidationPair, 0);
    }
```

https://github.com/GenerationSoftware/pt-v5-prize-pool/blob/4bc8a12b857856828c018510b5500d722b79ca3a/src/PrizePool.sol#L673-L675

## Tools Used
Manual Review and VSCode

## Recommended Mitigation Steps

Hence it is recommended to add the `pause` or `timelock` functionality to the `liquidate` function, so that the owner can `pause` the `liquidate` function before calling the `Vault.setLiquidationPair` function to change the `_liquidationPair` contract.


## Assessed type

Other
An identical vault can be deployed with existing values, the logic controlling this is missing
Audit: 2023-07-pooltogether-findings
Severity: MEDIUM

# Lines of code

https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/VaultFactory.sol#L55-L86


# Vulnerability details

`VaultFactory. The deployVault()` function deploys a new vault with 10 arguments, but does not check if there is a vault already deployed with the same arguments

This seems to have been preferred as a design, but malicious people with copy safes can direct users to the wrong safes and face unpredictable risks.

```solidity
vault/src/VaultFactory.sol:
  54     */
  55:   function deployVault(
  56:     IERC20 _asset,
  57:     string memory _name,
  58:     string memory _symbol,
  59:     TwabController _twabController,
  60:     IERC4626 _yieldVault,
  61:     PrizePool _prizePool,
  62:     address _claimer,
  63:     address _yieldFeeRecipient,
  64:     uint256 _yieldFeePercentage,
  65:     address _owner
  66:   ) external returns (address) {
  67:     Vault _vault = new Vault(
  68:       _asset,
  69:       _name,
  70:       _symbol,
  71:       _twabController,
  72:       _yieldVault,
  73:       _prizePool,
  74:       _claimer,
  75:       _yieldFeeRecipient,
  76:       _yieldFeePercentage,
  77:       _owner
  78:     );
  79: 
  80:     allVaults.push(_vault);
  81:     deployedVaults[_vault] = true;
  82: 
  83:     emit NewFactoryVault(_vault, VaultFactory(address(this)));
  84: 
  85:     return address(_vault);
  86:   }
  87: 
  87 
```

**Recommendation:**

If there is an existing safe with the same 10 arguments, revert the transaction



## Assessed type

Access Control