Lucene search

K
code423n4Code4renaCODE423N4:2021-12-POOLTOGETHER-FINDINGS-ISSUES-93
HistoryDec 12, 2021 - 12:00 a.m.

Attacker can empty all the funds by creating fake promotions

2021-12-1200:00:00
Code4rena
github.com
5

Handle

WatchPug

Vulnerability details

The current implementation of _calculateRewardAmount allows a arbitrary _epochId, which can even be a _epochId > _numberOfEpochs.

A malicious user can call claimRewards() with _epochIds larger than _numberOfEpochs and claim other users’ rewards.

Furthermore, since claimRewards() will use the wallet balance of promotion.token without limiting the total amount of funds used to be less than or equal to the total rewards added to this promotion.

An attacker can create a fake promotion with the same reward token, the attacker can steal all the funds for other promotions or actually all the ERC20 tokens held by the contract by creating fake promotion with _numberOfEpochs set to 0. The attacker may or may not use a fake ticket.

<https://github.com/pooltogether/v4-periphery/blob/0e94c54774a6fce29daf9cb23353208f80de63eb/contracts/TwabRewards.sol#L289-L324&gt;

function _calculateRewardAmount(
        address _user,
        Promotion memory _promotion,
        uint256 _epochId
    ) internal view returns (uint256) {
        uint256 _epochDuration = _promotion.epochDuration;
        uint256 _epochStartTimestamp = _promotion.startTimestamp + (_epochDuration * _epochId);
        uint256 _epochEndTimestamp = _epochStartTimestamp + _epochDuration;

        require(block.timestamp &gt; _epochEndTimestamp, "TwabRewards/epoch-not-over");

        ITicket _ticket = ITicket(_promotion.ticket);

        uint256 _averageBalance = _ticket.getAverageBalanceBetween(
            _user,
            uint64(_epochStartTimestamp),
            uint64(_epochEndTimestamp)
        );

        uint64[] memory _epochStartTimestamps = new uint64[](1);
        _epochStartTimestamps[0] = uint64(_epochStartTimestamp);

        uint64[] memory _epochEndTimestamps = new uint64[](1);
        _epochEndTimestamps[0] = uint64(_epochEndTimestamp);

        uint256[] memory _averageTotalSupplies = _ticket.getAverageTotalSuppliesBetween(
            _epochStartTimestamps,
            _epochEndTimestamps
        );

        if (_averageTotalSupplies[0] &gt; 0) {
            return (_promotion.tokensPerEpoch * _averageBalance) / _averageTotalSupplies[0];
        }

        return 0;
    }

<https://github.com/pooltogether/v4-periphery/blob/0e94c54774a6fce29daf9cb23353208f80de63eb/contracts/TwabRewards.sol#L162-L191&gt;

function claimRewards(
    address _user,
    uint256 _promotionId,
    uint256[] calldata _epochIds
) external override returns (uint256) {
    Promotion memory _promotion = _getPromotion(_promotionId);

    uint256 _rewardsAmount;
    uint256 _userClaimedEpochs = _claimedEpochs[_promotionId][_user];

    for (uint256 index = 0; index &lt; _epochIds.length; index++) {
        uint256 _epochId = _epochIds[index];

        require(
            !_isClaimedEpoch(_userClaimedEpochs, _epochId),
            "TwabRewards/rewards-already-claimed"
        );

        _rewardsAmount += _calculateRewardAmount(_user, _promotion, _epochId);
        _userClaimedEpochs = _updateClaimedEpoch(_userClaimedEpochs, _epochId);
    }

    _claimedEpochs[_promotionId][_user] = _userClaimedEpochs;

    _promotion.token.safeTransfer(_user, _rewardsAmount);

    emit RewardsClaimed(_promotionId, _epochIds, _user, _rewardsAmount);

    return _rewardsAmount;
}

PoC

Given:

  • The attacker owns 1,000 ATicket
  • totalSupply of ATicket = 10,000
  1. Alice created Promotion 1 with the following parameters, transferred 12,000 USDC to the contract:
  • _token = USDC
  • _tokensPerEpoch = 1,000
  • _numberOfEpochs = 12
  • _epochDuration = 30 days
  1. The attacker created Promotion 2 right after Alice with the following parameters, trasnferred 0 USDC to the contract:
  • _ticket = ATicket
  • _token = USDC
  • _tokensPerEpoch = 120,000
  • _numberOfEpochs = 0
  • _epochDuration = 1
  1. The attacker calls claimRewards() with, recive 12,000 USDC as rewards:
  • _promotionId = 2
  • _epochIds = [β€œ0”]

Recommendation

Change to:

function claimRewards(
    address _user,
    uint256 _promotionId,
    uint256[] calldata _epochIds
) external override returns (uint256) {
    Promotion memory _promotion = _getPromotion(_promotionId);

    uint256 _rewardsAmount;
    uint256 _userClaimedEpochs = _claimedEpochs[_promotionId][_user];
    uint256 _numberOfEpochs = _promotions[_promotionId].numberOfEpochs;
    for (uint256 index = 0; index &lt; _epochIds.length; index++) {
        uint256 _epochId = _epochIds[index];
        require(_epochId &lt; _numberOfEpochs, "!_epochId");
        require(
            !_isClaimedEpoch(_userClaimedEpochs, _epochId),
            "TwabRewards/rewards-already-claimed"
        );

        _rewardsAmount += _calculateRewardAmount(_user, _promotion, _epochId);
        _userClaimedEpochs = _updateClaimedEpoch(_userClaimedEpochs, _epochId);
    }

    _claimedEpochs[_promotionId][_user] = _userClaimedEpochs;

    _promotion.token.safeTransfer(_user, _rewardsAmount);

    emit RewardsClaimed(_promotionId, _epochIds, _user, _rewardsAmount);

    return _rewardsAmount;
}  

The text was updated successfully, but these errors were encountered:

All reactions