MorpheusAI

MorpheusAI
Foundry
22,500 USDC
View results
Submission Details
Severity: medium
Invalid

Griefing attack: Attacker can claim reward for any user, lead to loss of profit in extreme case

Summary

Attacker can claim reward for any user, lead to loss of profit in extreme case

Vulnerability Details

In Distribution contract, function claim() is used to claim reward. Anyone can claim reward for other user:

function claim(uint256 poolId_, address user_) external payable poolExists(poolId_) {
    Pool storage pool = pools[poolId_];
    PoolData storage poolData = poolsData[poolId_];
    UserData storage userData = usersData[user_][poolId_];

    require(block.timestamp > pool.payoutStart + pool.claimLockPeriod, "DS: pool claim is locked");

    uint256 currentPoolRate_ = _getCurrentPoolRate(poolId_);   //<---
    uint256 pendingRewards_ = _getCurrentUserReward(currentPoolRate_, userData);
    require(pendingRewards_ > 0, "DS: nothing to claim");

    // Update pool data
    poolData.lastUpdate = uint128(block.timestamp);
    poolData.rate = currentPoolRate_;

    // Update user data
    userData.rate = currentPoolRate_;
    userData.pendingRewards = 0;

    // Transfer rewards
    L1Sender(l1Sender).sendMintMessage{value: msg.value}(user_, pendingRewards_, _msgSender());

    emit UserClaimed(poolId_, user_, pendingRewards_);
}

And pool rate is calculated as below:

function _getCurrentPoolRate(uint256 poolId_) private view returns (uint256) {
    PoolData storage poolData = poolsData[poolId_];  

    if (poolData.totalDeposited == 0) {
        return poolData.rate;
    }

    uint256 rewards_ = getPeriodReward(poolId_, poolData.lastUpdate, uint128(block.timestamp));  //<-----

    return poolData.rate + (rewards_ * PRECISION) / poolData.totalDeposited;
}

And function getPeriodReward() use data from pools storage to calculate period reward:

function getPeriodReward(uint256 poolId_, uint128 startTime_, uint128 endTime_) public view returns (uint256) {
    if (!_poolExists(poolId_)) {
        return 0;
    }

    Pool storage pool = pools[poolId_];   // <----

    return
        LinearDistributionIntervalDecrease.getPeriodReward(  // <---
            pool.initialReward,
            pool.rewardDecrease,
            pool.payoutStart,
            pool.decreaseInterval,
            startTime_,
            endTime_
        );
}

And it can be manipulated by owner:

function editPool(uint256 poolId_, Pool calldata pool_) external onlyOwner poolExists(poolId_) {
    _validatePool(pool_);
    require(pools[poolId_].isPublic == pool_.isPublic, "DS: invalid pool type");

    PoolData storage poolData = poolsData[poolId_];
    uint256 currentPoolRate_ = _getCurrentPoolRate(poolId_);

    // Update pool data
    poolData.rate = currentPoolRate_;
    poolData.lastUpdate = uint128(block.timestamp);

    pools[poolId_] = pool_;  // <---

    emit PoolEdited(poolId_, pool_);
}

LinearDistributionIntervalDecrease.getPeriodReward() function:

    uint256 firstPeriodReward_ = _calculatePartPeriodReward(
        payoutStart_,
        startTime_,
        interval_,
        initialAmount_,
        decreaseAmount_,
        true
    );

    uint256 secondPeriodReward_ = _calculateFullPeriodReward(
        payoutStart_,
        startTime_,
        endTime_,
        interval_,
        initialAmount_,
        decreaseAmount_
    );

    uint256 thirdPeriodReward_ = _calculatePartPeriodReward(
        payoutStart_,
        endTime_,
        interval_,
        initialAmount_,
        decreaseAmount_,
        false
    );
    return firstPeriodReward_ + secondPeriodReward_ + thirdPeriodReward_;

In both _calculatePartPeriodReward() and _calculateFullPeriodReward() function, there is a condition like this:

    if (decreaseRewardAmount_ >= initialAmount_) {
        return 0;
    }

So the impact is user's rate is updated, but do not have any reward

Consider scenario:

  • Owner update pools data of pool id that have benefit for user

  • Attacker front-running that update, claim rewards for user

  • User will lost reward that they should have if they claim after update

Impact

User will lose reward that they should have

Tools Used

Manual review

Recommendations

Only user can claim reward of themself:

function claim(uint256 poolId_, address user_) external payable poolExists(poolId_) {/
  + require(msg.sender == user_);
Updates

Lead Judging Commences

inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice
greatlake Submitter
over 1 year ago
inallhonesty Lead Judge
over 1 year ago
greatlake Submitter
over 1 year ago
inallhonesty Lead Judge
over 1 year ago
inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.