code-423n4 / 2024-03-abracadabra-money-findings

9 stars 7 forks source link

`LockingMultiRewards` contract: users will be able to claim more rewards than they are entitled to by calling `withdrawWithRewards()` #180

Closed c4-bot-5 closed 7 months ago

c4-bot-5 commented 8 months ago

Lines of code

https://github.com/code-423n4/2024-03-abracadabra-money/blob/1f4693fdbf33e9ad28132643e2d6f7635834c6c6/src/staking/LockingMultiRewards.sol#L186-L189

Vulnerability details

Impact

    function withdrawWithRewards(uint256 amount) public virtual {
            withdraw(amount);
            _getRewards(msg.sender);
    }

where first the withdrawal of the unlocked staked amount is executed, but before that the accumulated rewards of the user are updated via _updateRewardsForUser(), then his unlocked balance is decreased by the withdrawn amount:

          function withdraw(uint256 amount) public virtual {
                  if (amount == 0) {
                      revert ErrZeroAmount();
                  }

                  _updateRewardsForUser(msg.sender);

                  _balances[msg.sender].unlocked -= amount;
                  unlockedSupply -= amount;

                  stakingToken.safeTransfer(msg.sender, amount);
                  stakingTokenBalance -= amount;

                  emit LogWithdrawn(msg.sender, amount);
              }
    function _updateRewardsForUser(address user) internal {
        uint256 balance = balanceOf(user); // << unlocked + locked*3
        uint256 totalSupply_ = totalSupply(); //<< unlocked + locked*3

        for (uint256 i; i < rewardTokens.length; ) {
            address token = rewardTokens[i];
            _udpateUserRewards(user, balance, token, _updateRewardsGlobal(token, totalSupply_));

            unchecked {
                ++i;
            }
        }
    }

then after the withdrawal is executed, the _getRewards() function is invoked to transfer the rewards for the user.

Proof of Concept

LockingMultiRewards.withdrawWithRewards function

    function withdrawWithRewards(uint256 amount) public virtual {
        withdraw(amount);
        _getRewards(msg.sender);
    }

Tools Used

Manual Review.

Recommended Mitigation Steps

Update withdrawWithRewards() function to update user's uncalimed rewards based on his current balance (similar mechanism to the getRewards() function):

    function withdrawWithRewards(uint256 amount) public virtual {
        withdraw(amount);
+       _updateRewardsForUser(msg.sender);
        _getRewards(msg.sender);
    }

Assessed type

Context

c4-pre-sort commented 8 months ago

141345 marked the issue as insufficient quality report

141345 commented 8 months ago

the mitigation makes no diff

c4-judge commented 7 months ago

thereksfour marked the issue as unsatisfactory: Insufficient proof

DevHals commented 7 months ago

Hi @thereksfour ,

this issue clearly states that withdrawing and claiming rewards via withdrawWithRewards() will result in claiming more rewards as it uses the old balance of the user before he withdrew:

thereksfour commented 7 months ago

Invalid, the added _updateRewardsForUser in the recommendation will do nothing, because userRewardPerTokenPaid has been updated in withdraw, which makes the _earned of _updateRewardsForUser in the recommendation return 0. Also, if you disagree, please provide a step-by-step POC to illustrate the issue.