Skip to content

Latest commit

 

History

History
127 lines (91 loc) · 3.61 KB

File metadata and controls

127 lines (91 loc) · 3.61 KB

Basic Lilac Marmot

High

steal reward token through StakingPoolToken.sol

Summary

Summary

function stake(address _user, uint256 _amount) external override {
  require(stakingToken != address(0), "I");
  if (stakeUserRestriction != address(0)) {
      require(_user == stakeUserRestriction, "U");
  }
  _mint(_user, _amount);
  IERC20(stakingToken).safeTransferFrom(_msgSender(), address(this), _amount);
  emit Stake(_msgSender(), _user, _amount);
}

function unstake(uint256 _amount) external override {
  _burn(_msgSender(), _amount);
  IERC20(stakingToken).safeTransfer(_msgSender(), _amount);
  emit Unstake(_msgSender(), _amount);
}

The stake function cannot be directly called when stakeUserRestriction is set, but it can be called through the indexUtils contract. After calling the stake function, the unstake function can be invoked, triggering both the _burn function and the _update function.

function _update(address _from, address _to, uint256 _value) internal override {
  super._update(_from, _to, _value);
  if (_from != address(0)) {
      TokenRewards(POOL_REWARDS).setShares(_from, _value, true);
  }
  if (_to != address(0) && _to != address(0xdead)) {
      TokenRewards(POOL_REWARDS).setShares(_to, _value, false);
  }
}

When the _update function is called, it invokes setShares from POOL_REWARDS, adding the value of shares to the from address.

function setShares(address _wallet, uint256 _amount, bool _sharesRemoving) external override {
  require(_msgSender() == trackingToken, "UNAUTHORIZED");
  _setShares(_wallet, _amount, _sharesRemoving);
}

function _setShares(address _wallet, uint256 _amount, bool _sharesRemoving) internal {
  _processFeesIfApplicable();
  if (_sharesRemoving) {
      _removeShares(_wallet, _amount);
      emit RemoveShares(_wallet, _amount);
  } else {
      _addShares(_wallet, _amount);
      emit AddShares(_wallet, _amount);
  }
}

The setShares function adds the specified amount of shares to the _wallet address's share storage.

function _distributeReward(address _wallet) internal {
  if (shares[_wallet] == 0) {
      return;
  }
  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);
      }
  }
}

The claimReward function calls _distributeReward in the TokenRewards contract, allowing tokens to be stolen in this process.

Root Cause

https://github.com/sherlock-audit/2025-01-peapods-finance/blob/main/contracts/contracts/StakingPoolToken.sol#L77-L81

The system is designed to give rewards when the unstake function is called, but since unstake can be called at any time, this can be exploited to withdraw all rewards.

Internal Pre-conditions

No response

External Pre-conditions

No response

Attack Path

The attack sequence is as follows:

  1. The attacker calls the stake and unstake functions to trigger the setShares function in POOL_REWARDS.
  2. Once step 1 is completed, the attacker can withdraw tokens through claimRewards in the TokenRewards contract.
  3. The above steps are repeated to steal all the tokens.

Impact

can steal all rewards

PoC

No response

Mitigation

No response