Skip to content

Latest commit

 

History

History
127 lines (89 loc) · 3.85 KB

File metadata and controls

127 lines (89 loc) · 3.85 KB

Fast Khaki Raccoon

High

Pausing rewards will lead to tokens being bricked and users not being able to claim them

Summary

When adding or removing shares for tokens if one of them is paused it would cause all rewards, even the ones accumulated before the pause to be "bricked" for all users who claim during the pause.

Example would be _removeShares

https://github.com/sherlock-audit/2025-01-peapods-finance/blob/main/contracts/contracts/TokenRewards.sol#L126-L135

    function _removeShares(address _wallet, uint256 _amount) internal {
        require(shares[_wallet] > 0 && _amount <= shares[_wallet], "RE");
        _distributeReward(_wallet);
        totalShares -= _amount;
        shares[_wallet] -= _amount;

        if (shares[_wallet] == 0) {
            totalStakers--;
        }

        _resetExcluded(_wallet);
    }

for we first call _distributeReward and distribute for all not paused tokens

https://github.com/sherlock-audit/2025-01-peapods-finance/blob/main/contracts/contracts/TokenRewards.sol#L240-L255

        for (uint256 _i; _i < _allRewardsTokens.length; _i++) {
            address _token = _allRewardsTokens[_i];

            if (REWARDS_WHITELISTER.paused(_token)) {
                continue;
            }

            uint256 _amount = getUnpaid(_token, _wallet);
            rewards[_token][_wallet].realized += _amount;

            rewards[_token][_wallet].excluded = _cumulativeRewards(_token, shares[_wallet], true);

            if (_amount > 0) {
                rewardsDistributed[_token] += _amount;
                IERC20(_token).safeTransfer(_wallet, _amount);
                emit DistributeReward(_wallet, _token, _amount);
            }
        }

but then trigger _resetExcluded and reset rewards[_token][_wallet].excluded` for all tokens.

Root Cause

The diff in handling between _resetExcluded and _distributeReward when it comes to paused rewards

    function _resetExcluded(address _wallet) internal {
        for (uint256 _i; _i < _allRewardsTokens.length; _i++) {
            address _token = _allRewardsTokens[_i];
            rewards[_token][_wallet].excluded = _cumulativeRewards(_token, shares[_wallet], true);
        }
    }
        for (uint256 _i; _i < _allRewardsTokens.length; _i++) {
            address _token = _allRewardsTokens[_i];

            if (REWARDS_WHITELISTER.paused(_token)) {
                continue;
            }

            uint256 _amount = getUnpaid(_token, _wallet);
            rewards[_token][_wallet].realized += _amount;

            rewards[_token][_wallet].excluded = _cumulativeRewards(_token, shares[_wallet], true);

            if (_amount > 0) {
                rewardsDistributed[_token] += _amount;
                IERC20(_token).safeTransfer(_wallet, _amount);
                emit DistributeReward(_wallet, _token, _amount);
            }
        }

Internal Pre-conditions

No response

External Pre-conditions

No response

Attack Path

  1. Token A and B both accumulate 1 reward per share
  2. Alice has 100 shares and claims rewards for both tokens and her rewards[token][Alice].excluded are synced with the current rewards
  3. B is then temporarily paused for some reason
  4. Bob claims rewards, however since B is paused he claims only for A
  5. Bob's rewards[token][Alice].excluded are both synced since _resetExcluded does not have any way to handle paused tokens
    function _resetExcluded(address _wallet) internal {
        for (uint256 _i; _i < _allRewardsTokens.length; _i++) {
            address _token = _allRewardsTokens[_i];
            rewards[_token][_wallet].excluded = _cumulativeRewards(_token, shares[_wallet], true);
        }
    }

Impact

Users will lose on rewards. Tokens will be bricked.

PoC

No response

Mitigation

Include the same mechanic inside _resetExcluded.