PoolTogether - Nyx'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: 16/111

Findings: 3

Award: $1,665.52

🌟 Selected for report: 1

🚀 Solo Findings: 1

Awards

2.2492 USDC - $2.25

Labels

bug
3 (High Risk)
satisfactory
duplicate-396

External Links

Lines of code

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

Vulnerability details

Impact

Anyone can call mintYieldFee and steal funds.

Proof of Concept

if (_yieldFeePercentage != 0) {
      _increaseYieldFeeBalance(
        (_amountOut * FEE_PRECISION) / (FEE_PRECISION - _yieldFeePercentage) - _amountOut
      );
    }
function _increaseYieldFeeBalance(uint256 _shares) internal {
    _yieldFeeTotalSupply += _shares;
  }

After using liquidate() function, _yieldFeeTotalSupply can be increased. The yield fee can serve as a buffer in case of undercollateralization of the Vault.

function mintYieldFee(uint256 _shares, address _recipient) external { //@audit anyone can call this ?
    _requireVaultCollateralized();
    if (_shares > _yieldFeeTotalSupply) revert YieldFeeGTAvailable(_shares, _yieldFeeTotalSupply);

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

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

But when undercollateralization is over, anyone can call mintYieldFee() function and steal the funds.

Tools Used

Manual Review

Access Control

Assessed type

Access Control

#0 - c4-judge

2023-07-14T22:20:06Z

Picodes marked the issue as duplicate of #396

#1 - c4-judge

2023-08-05T22:04:46Z

Picodes marked the issue as satisfactory

Findings Information

Awards

19.2867 USDC - $19.29

Labels

bug
2 (Med Risk)
satisfactory
duplicate-431

External Links

Lines of code

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

Vulnerability details

Impact

Anyone can call setDrawManager() and be drawManager.

Proof of Concept

constructor(
    ConstructorParams memory params
  )
    TieredLiquidityDistributor(
      params.numberOfTiers,
      params.tierShares,
      params.canaryShares,
      params.reserveShares
    )
  {
    if (unwrap(params.smoothing) >= unwrap(UNIT)) {
      revert SmoothingGTEOne(unwrap(params.smoothing));
    }
    prizeToken = params.prizeToken;
    twabController = params.twabController;
    smoothing = params.smoothing;
    claimExpansionThreshold = params.claimExpansionThreshold;
    drawPeriodSeconds = params.drawPeriodSeconds;
    _lastClosedDrawStartedAt = params.firstDrawStartsAt;

    drawManager = params.drawManager; //@audit here
    if (params.drawManager != address(0)) {
      emit DrawManagerSet(params.drawManager);
    }
  }

If drawManager is not set in the constructor, anyone can call setDrawManager() function and can be drawManager.

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

    emit DrawManagerSet(_drawManager);
  }

After became drawManager, attacker can steal funds with withdrawReserve() function.

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

Tools Used

Manual Review

Access Control

Assessed type

Access Control

#0 - c4-judge

2023-07-16T22:24:39Z

Picodes marked the issue as duplicate of #356

#1 - c4-judge

2023-08-06T10:32:14Z

Picodes marked the issue as satisfactory

Findings Information

🌟 Selected for report: Nyx

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
selected for report
sponsor confirmed
edited-by-warden
M-13

Awards

1643.9812 USDC - $1,643.98

External Links

Lines of code

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

Vulnerability details

Impact

Users can alter the record of their balances.

Proof of Concept

If users delegateBalance has changed, observations will be recorded. If there is a need for new observation, new observation will be created otherwise will be overwritten.

And new observation creation will be decided in the _getNextObservationIndex() function.

if (currentPeriod == 0 || currentPeriod > newestObservationPeriod) {
      return (
        uint16(RingBufferLib.wrap(_accountDetails.nextObservationIndex, MAX_CARDINALITY)),
        newestObservation,
        true
      );
    }

newestObservationPeriod is the last observations period. currentPeriod is a period that calculated with uint32 currentTime = uint32(block.timestamp).

uint32 currentPeriod = _getTimestampPeriod(PERIOD_LENGTH, PERIOD_OFFSET, currentTime);
    uint32 newestObservationPeriod = _getTimestampPeriod(
      PERIOD_LENGTH,
      PERIOD_OFFSET,
      newestObservation.timestamp
    );
function _getTimestampPeriod(
    uint32 PERIOD_LENGTH,
    uint32 PERIOD_OFFSET,
    uint32 _timestamp
  ) private pure returns (uint32 period) {
    if (_timestamp <= PERIOD_OFFSET) {
      return 0;
    }
    // Shrink by 1 to ensure periods end on a multiple of PERIOD_LENGTH.
    // Increase by 1 to start periods at # 1.
    return ((_timestamp - PERIOD_OFFSET - 1) / PERIOD_LENGTH) + 1;
  }

The problem is, If someone deposits a small amount frequently, currentPeriod and newestObservationPeriod will always be the same and new observation won't be created. Attackers can keep doing this until closeDraw and manipulate their balances.

According to the docs : If a draw were to start and end within a period a user would be able to alter the record of their balance for that draw by overwriting an Observation.

It is important to note that due to Observation overwriting, average balances for a period are not finalized until a period ends. Therefore if a draw ends but a period has not, a user would be able to manipulate their average balance for that final period of time after the draw ends. This would result in an inaccurate record of their balance held during the draw.

Tools Used

Manual Review

Assessed type

Other

#0 - c4-sponsor

2023-07-20T21:56:25Z

asselstine marked the issue as sponsor confirmed

#1 - c4-judge

2023-08-06T22:20:37Z

Picodes changed the severity to 2 (Med Risk)

#2 - c4-judge

2023-08-06T22:23:43Z

Picodes changed the severity to 3 (High Risk)

#3 - c4-judge

2023-08-06T22:23:47Z

Picodes marked the issue as satisfactory

#4 - c4-judge

2023-08-06T22:25:35Z

Picodes changed the severity to 2 (Med Risk)

#5 - Picodes

2023-08-06T22:29:34Z

If someone deposits a small amount frequently, currentPeriod and newestObservationPeriod will always be the same and new observation won't be created-> this only works for small deposit within the same PERIOD_LENGTH, otherwise timestamp roundings will differ.

This reports shows how by leveraging the fact that new observations are not created if the previous observations falls within the same period, an attacker could modify its average balance for the final period if a draw ends within a period.

#6 - asselstine

2023-08-17T21:35:28Z

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