PoolTogether - Udsen's results

A protocol for no-loss prize savings

General Information

Platform: Code4rena

Start Date: 07/07/2023

Pot Size: $121,650 USDC

Total HM: 36

Participants: 111

Period: 7 days

Judge: Picodes

Total Solo HM: 13

Id: 258

League: ETH

PoolTogether

Findings Distribution

Researcher Performance

Rank: 30/111

Findings: 5

Award: $739.38

Gas:
grade-a

🌟 Selected for report: 3

🚀 Solo Findings: 0

Awards

2.9239 USDC - $2.92

Labels

bug
3 (High Risk)
primary issue
satisfactory
selected for report
sponsor confirmed
H-04

External Links

Lines of code

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

Vulnerability details

Impact

The Vault.mintYieldFee external function is used to mint Vault shares to the yield fee _recipient. The function is an external function and can be called by anyone since there is no access control. The function will revert only under following two conditions:

  1. if the Vault is undercollateralized
  2. if the _shares are greater than the accrued _yieldFeeTotalSupply.

The issue with this function is it allows the caller to set the _recipient (Address of the yield fee recipient). It does not use the _yieldFeeRecipient state variable which was set in the Vault.constructor as the yield fee recipient.

Which means any one can steal the available yield fee from this vault (As long as above two revert conditions are not satisfied) by minting shares to his own address or to any address of his choice.

Proof of Concept

  function mintYieldFee(uint256 _shares, address _recipient) external {
    _requireVaultCollateralized();
    if (_shares > _yieldFeeTotalSupply) revert YieldFeeGTAvailable(_shares, _yieldFeeTotalSupply);

    _yieldFeeTotalSupply -= _shares;
    _mint(_recipient, _shares);

    emit MintYieldFee(msg.sender, _recipient, _shares);
  }

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

Tools Used

Manual Review and VSCode

Hence it is recommended to use the _yieldFeeRecipient state variable value as the yield fee recipient inside the Vault.mintYieldFee external function and to remove the input parameter address _recipient from the Vault.mintYieldFee function. So that the caller will not be able to mint shares to any arbitory address of his choice and steal the yield fee of the protocol.

The updated function should be as follows:

  function mintYieldFee(uint256 _shares) external {
    _requireVaultCollateralized();
    if (_shares > _yieldFeeTotalSupply) revert YieldFeeGTAvailable(_shares, _yieldFeeTotalSupply);

    _yieldFeeTotalSupply -= _shares;
    _mint(_yieldFeeRecipient, _shares);

    emit MintYieldFee(msg.sender, _recipient, _shares);
  } 

Assessed type

Other

#0 - c4-judge

2023-07-14T22:16:57Z

Picodes marked the issue as duplicate of #406

#1 - c4-judge

2023-07-14T22:17:05Z

Picodes marked the issue as selected for report

#2 - c4-sponsor

2023-07-20T22:33:33Z

asselstine marked the issue as sponsor confirmed

#3 - PierrickGT

2023-08-02T22:37:39Z

#4 - c4-judge

2023-08-05T22:01:39Z

Picodes marked the issue as satisfactory

Findings Information

🌟 Selected for report: Udsen

Also found by: qpzm, saneryee

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
sponsor confirmed
M-01

Awards

443.8749 USDC - $443.87

External Links

Lines of code

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

Vulnerability details

Impact

The Vault._deposit function is used by the users to deposit _assets to the vault and mint vault shares to the recipient address. The amount of _assets are transferred to the Vault as follows:

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

The Vault.deposit function uses this _assets amount to calculate the number of shares to be minted to the _recipient address.

The issue here is if the underlying _asset is a fee on transfer token then the actual received amount to the vault will be less than what is referred in the Vault.deposit function _assets input parameter. But the shares to mint is calculated using the entire _assets amount.

This issue could be further aggravated since the _asset is again deposited to the _yieldVault and when needing to be redeemed will be withdrawn from the _yieldVault as well. These operations will again charge a fee if the _asset is a fee on transfer token. Hence the actual left _asset amount for particular user will be less than the amount he initially transferred in.

Hence when the user redeems the minted shares back to the _assets, the contract will not have enough assets to transfer to the redeemer thus reverting the transaction.

Proof of Concept

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

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

    _yieldVault.deposit(_assets, address(this));

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

    _yieldVault.withdraw(_assets, address(this), address(this));
    SafeERC20.safeTransfer(IERC20(asset()), _receiver, _assets);

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

Tools Used

Manual Review and VSCode

Hence it is recommended to compute the _assets amount balance of the contract before and after the safeTransferFrom call and get the difference between the two as the actually transferred amount to the Vault. Then this actually transferred amount can be converted to shares and mint the correct amount of shares to the recipient.

  uint256 balanceBefore = _asset.balanceOf(address(this));

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

  uint256 balanceAfter = _asset.balanceOf(address(this));  

  uint256 transferredAmount = balanceAfter - balanceBefore;

Assessed type

Other

#0 - c4-judge

2023-07-16T21:48:08Z

Picodes marked the issue as primary issue

#1 - c4-sponsor

2023-07-20T23:39:35Z

asselstine marked the issue as sponsor confirmed

#2 - c4-judge

2023-08-07T15:12:20Z

Picodes marked the issue as satisfactory

#3 - c4-judge

2023-08-07T15:12:24Z

Picodes marked the issue as selected for report

#4 - Picodes

2023-08-08T10:09:09Z

Out of scope per the automated report: https://gist.github.com/itsmetechjay/e7fd03943bbacff1984a33b9f89c4149 (MEDIUM-4)

#5 - c4-judge

2023-08-08T10:10:46Z

Picodes marked the issue as unsatisfactory: Out of scope

#6 - udsene

2023-08-10T14:59:26Z

@Picodes, The fee on transfer token vulnerability described in this issue (#470) is related to the token transfers happening in the Vault.sol contract. How the fee charged on token transfers could effect the Vault._deposit , _yieldVault.deposit and _yieldVault.withdraw functions.

The MEDIUM-4 given in the automated report has found one instance of this vulnerability (fee on token transfer) and it is related to the PrizePool.sol contract and not the Vault.sol contract. It is not the same vulnerability present in the #470 issue.

#7 - Picodes

2023-08-12T16:00:00Z

Indeed. The automated report didn't properly flag that Vault.sol won't work with FoT.

#8 - c4-judge

2023-08-12T16:00:16Z

Picodes marked the issue as selected for report

#9 - c4-judge

2023-08-12T16:00:25Z

Picodes marked the issue as satisfactory

Findings Information

🌟 Selected for report: wvleak

Also found by: 0xbepresent, 3docSec, DadeKuma, Jeiwan, Udsen, dirk_y, keccak123, rvierdiiev, serial-coder, shaka, wvleak

Labels

bug
2 (Med Risk)
downgraded by judge
partial-50
duplicate-465

Awards

20.0427 USDC - $20.04

External Links

Lines of code

https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L618-L629 https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L1053 https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L1068

Vulnerability details

Impact

The Vault.claimPrizes function is used to claim prizes for the winners of the prizes. This is done by iterating through two nested for loops. One for loop iterates through the number of winners and other for loop iterates through the _prizeIndices.

Inside these nested loops the Vault._claimPrize() internal function is called. In the Vault._claimPrize() function, the hooks.useBeforeClaimPrize and hooks.implementation.afterClaimPrize hooks of a particular winner can be called. These hooks are set by the winner himself calling the Vault.setHooks function.

Hence a malicious winner can revert the transaction inside the hooks.useBeforeClaimPrize or hooks.implementation.afterClaimPrize hook function, thus making the entire prize claim for other winners to be Denied as well(DoS).

This will make it impossible for the _claimer to distribute the prizes among the winners.

Proof of Concept

    for (uint w = 0; w < _winners.length; w++) {
      uint prizeIndicesLength = _prizeIndices[w].length;
      for (uint p = 0; p < prizeIndicesLength; p++) {
        totalPrizes += _claimPrize(
          _winners[w],
          _tier,
          _prizeIndices[w][p],
          _feePerClaim,
          _feeRecipient
        );
      }
    }

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

      recipient = hooks.implementation.beforeClaimPrize(_winner, _tier, _prizeIndex);

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

      hooks.implementation.afterClaimPrize(

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

Tools Used

Manual Review and VSCode

Hence it is recommended to either omit the hook functionality from the winners or if hooks are an essential part of the protocol then it is recommended to allow the winner or a claimer to claim the prizes individually rather than iterating through an array of all winners.

Assessed type

DoS

#0 - Picodes

2023-07-15T08:31:28Z

What prevents the claimer from just skipping the malicious hooks using the _winners array?

#1 - c4-judge

2023-07-15T08:31:32Z

Picodes changed the severity to 2 (Med Risk)

#2 - c4-judge

2023-07-18T17:14:47Z

Picodes marked the issue as duplicate of #465

#3 - c4-judge

2023-08-07T15:13:25Z

Picodes marked the issue as satisfactory

#4 - c4-judge

2023-08-07T15:13:44Z

Picodes marked the issue as partial-50

Findings Information

Awards

25.0727 USDC - $25.07

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
M-06

External Links

Lines of code

https://github.com/GenerationSoftware/pt-v5-prize-pool/blob/4bc8a12b857856828c018510b5500d722b79ca3a/src/PrizePool.sol#L278-L281 https://github.com/GenerationSoftware/pt-v5-prize-pool/blob/4bc8a12b857856828c018510b5500d722b79ca3a/src/PrizePool.sol#L299-L306 https://github.com/GenerationSoftware/pt-v5-prize-pool/blob/4bc8a12b857856828c018510b5500d722b79ca3a/src/PrizePool.sol#L348 https://github.com/GenerationSoftware/pt-v5-prize-pool/blob/4bc8a12b857856828c018510b5500d722b79ca3a/src/PrizePool.sol#L335-L342

Vulnerability details

Impact

In the PrizePool.constructor the drawManager state variable is set as follows:

drawManager = params.drawManager;

But it accepts even if the params.drawManager == address(0). There is no input validation for the address(0) as well.

The PrizePool.setDrawManager function is used to set the drawManager if not already set. But the problem here is that there is no access control for this function and any one can call this. A malicious user can front run the valid drawManager assignment transaction and set a malicious address as the drawManager. The other issue is once the drawManager is set it can not be updated due to the following conditional check. Hence the contract will have to redeployed.

if (drawManager != address(0)) { //@audit-issue - can be front run by a malicious user revert DrawManagerAlreadySet(); }

Hence the following two functions controlled by the onlyDrawManager modifier will be vulnerable to attacks, since the drawManager is a malicious address now.

  1. PrizePool.withdrawReserve can withdraw tokens from the reserve to any address given in the _to parameter. Hence this function will be vulnerable.

  2. PrizePool.closeDraw() function can close the current open draw. This function is only callable by the drawManager. Hence a malicious user can get control of this function thus making this function vulnerable.

Proof of Concept

    drawManager = params.drawManager;
    if (params.drawManager != address(0)) {
      emit DrawManagerSet(params.drawManager);
    }

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

  function setDrawManager(address _drawManager) external {
    if (drawManager != address(0)) {
      revert DrawManagerAlreadySet();
    }
    drawManager = _drawManager;

    emit DrawManagerSet(_drawManager);
  }

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

  function closeDraw(uint256 winningRandomNumber_) external onlyDrawManager returns (uint16) {

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

  function withdrawReserve(address _to, uint104 _amount) external onlyDrawManager {
    if (_amount > _reserve) {
      revert InsufficientReserve(_amount, _reserve);
    }
    _reserve -= _amount;
    _transfer(_to, _amount);
    emit WithdrawReserve(_to, _amount);
  }

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

Tools Used

Manaul Review and VSCode

Hence it is recommended to check for address(0) in the constructor when drawManager is set or to implement access control in the PrizePool.setDrawManager function so that only the admin of the contract can call this function to set the drawManager if it is not set already.

Assessed type

Invalid Validation

#0 - c4-judge

2023-07-16T16:16:23Z

Picodes marked the issue as duplicate of #356

#1 - c4-judge

2023-08-06T10:32:06Z

Picodes marked the issue as satisfactory

#2 - c4-judge

2023-08-06T10:33:03Z

Picodes marked the issue as selected for report

#3 - asselstine

2023-08-17T21:22:02Z

Findings Information

Awards

247.4795 USDC - $247.48

Labels

bug
G (Gas Optimization)
grade-a
sponsor confirmed
G-04

External Links

1. ARITHMETIC OPERATIONS CAN BE UNCHECKED DUE TO PREVIOUS CONDITIONAL CHECKS

_reserve -= _amount; can be unchecked to save gas, since previous conditional check makes sure that _amount <= _reserve.

https://github.com/GenerationSoftware/pt-v5-prize-pool/blob/4bc8a12b857856828c018510b5500d722b79ca3a/src/PrizePool.sol#L336-L339 https://github.com/GenerationSoftware/pt-v5-prize-pool/blob/4bc8a12b857856828c018510b5500d722b79ca3a/src/abstract/TieredLiquidityDistributor.sol#L671 https://github.com/GenerationSoftware/pt-v5-prize-pool/blob/4bc8a12b857856828c018510b5500d722b79ca3a/src/abstract/TieredLiquidityDistributor.sol#L538 https://github.com/GenerationSoftware/pt-v5-prize-pool/blob/4bc8a12b857856828c018510b5500d722b79ca3a/src/PrizePool.sol#L459 https://github.com/GenerationSoftware/pt-v5-prize-pool/blob/4bc8a12b857856828c018510b5500d722b79ca3a/src/PrizePool.sol#L490

2. REDUNDANT MSTORE ASSIGNMENT IN THE PrizePool._computeNextNumberOfTiers() function.

In the PrizePool._computeNextNumberOfTiers() the following code snippet is used to update the _nextNumberOfTiers value as shown below:

_nextNumberOfTiers = _nextNumberOfTiers > MINIMUM_NUMBER_OF_TIERS ? _nextNumberOfTiers : MINIMUM_NUMBER_OF_TIERS;

There is a redundant assignment of the _nextNumberOfTiers variable to itself when _nextNumberOfTiers > MINIMUM_NUMBER_OF_TIERS.

This can be optimized as follows:

if(!(_nextNumberOfTiers > MINIMUM_NUMBER_OF_TIERS)){ _nextNumberOfTiers = MINIMUM_NUMBER_OF_TIERS; }

This will save an extra MLOAD.

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

3. USE LOCAL MEMORY VARIABLE INSTEAD OF STATE VARIABLE

The PrizePool._computeVaultTierDetails function is used to compute the data to determine the winner of a prize. In the function there is an internal call made to teh _tierOdds function as shown below:

tierOdds = _tierOdds(_tier, numberOfTiers);

The numberOfTiers used in this function call is a state variable. Inplace of this state variable the _numberOfTiers memory variable can be used instead. This will save an extra SLOAD.

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

4. ARITHMETIC OPERATION CAN BE SIMPLIFIED TO SAVE GAS

The Vault._currentExchangeRate function, is used to calculate the exchange rate between the amount of assets withdrawable from the YieldVault and the amount of shares minted by this Vault. In the function there is a calculation as follows to compute the _withdrawableAssets when _withdrawableAssets > _totalSupplyToAssets, as shown below:

_withdrawableAssets = _withdrawableAssets - (_withdrawableAssets - _totalSupplyToAssets);

Here the yield is removed from the _withdrawableAssets amount.

The above calculation can be simplified as follows to save gas: and the above calculation can be commented for ease of understanding later.

_withdrawableAssets = _totalSupplyToAssets;

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

5. REDUNDANT CONDITIONAL CHECK CAN BE OMITTED TO SAVE GAS

In the Vault.liquidate function the following check is performed to verify that _tokenOut is the vault share.

if (_tokenOut != address(this)) revert LiquidationTokenOutNotVaultShare(_tokenOut, address(this));

But the same check check is performed inside the _liquidatableBalanceOf(_tokenOut) function call as well. Hence the above mentioned check inside the Vault.liquidate is a redundant check that can be omitted to save gas.

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

6. ARRAY LENGTH MISMATCH IN THE Vault.claimPrizes COULD LEAD TO WASTE OF GAS

Vault.claimPrizes is used to calim the prizes for the winners. There are two arrays used in the function as shown below:

address[] calldata _winners, uint32[][] calldata _prizeIndices,

These two arrays are used iterate through inside a nested for loop. But before the for loop the length of the above two arrays are not checked for equality. Hence if there is an array length mismath the transaction will revert after entering the for loop thus leading to a considerable gas loss to the _claimer.

Hence it is recommended to check _winners.length == _prizeIndices.length at the beginning of the function before entering the for loop.

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

#0 - c4-judge

2023-07-18T18:55:12Z

Picodes marked the issue as grade-a

#1 - c4-sponsor

2023-07-20T23:11:49Z

asselstine marked the issue as sponsor confirmed

#2 - PierrickGT

2023-09-08T23:50:18Z

1 - Fixed in the following PR: https://github.com/GenerationSoftware/pt-v5-prize-pool/pull/63 2, 3, 4, 5 - Has been fixed 6 - Arrays are not of the same length

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter