sherlock-audit / 2024-07-kwenta-staking-contracts-judging

1 stars 0 forks source link

Mean Concrete Eagle - Stakers will loss USDC Rewards on every reward notification #176

Closed sherlock-admin3 closed 1 month ago

sherlock-admin3 commented 1 month ago

Mean Concrete Eagle

Low/Info

Stakers will loss USDC Rewards on every reward notification

Summary

The calculation of the reward rate for USDC (rewardRateUSDC = _rewardUsdc / rewardsDuration) in the notifyRewardAmount function causes each staker to lose upto $0.6 worth of rewards for every reward notified due to precision loss.

With no recovery option, those USDC tokens will remain stuck in the contract indefinitely.

Root Cause

In notifyRewardAmount function, The calculation for USDC reward rate is provided below:


    function notifyRewardAmount(uint256 _reward, uint256 _rewardUsdc)
        external
        onlyRewardsNotifier
        updateReward(address(0))
    {
        if (block.timestamp >= periodFinish) {
            rewardRate = _reward / rewardsDuration;
@->         rewardRateUSDC = _rewardUsdc / rewardsDuration;
        } else {
            // SNIP

            uint256 leftoverUsdc = remaining * rewardRateUSDC;
            rewardRateUSDC = (_rewardUsdc + leftoverUsdc) / rewardsDuration;
        }

        // SNIP
    }

Link to Code

Focus point is formula:

rewardRateUSDC = _rewardUsdc / rewardsDuration

Here,

The issue occurs here because there is no sufficient wrapping of the amount before dividing by rewardsDuration. The number is divided and later multiplied by elapsed time here, causing a loss of precision of the amount modulo remaining time.

For kwenta token, the calculation is done by: rewardRate = _reward / rewardsDuration as reward here has 18 decimals so the precision loss is negligible. But for USDC with 6 decimal precision, the reward amount should be scaled to 18 decimals to avoid significant precision loss.

To make matter worst, recoverERC20 function allows only kwenta tokens to be rescued and not USDC tokens. So this unrewarded amount remains stuck in the contract indefinitely.

Internal pre-conditions

No response

External pre-conditions

No response

Attack Path

  1. Alice Stakes some kwenta tokens.

  2. New reward get notified.

  3. The USDC reward rate is calculated as: rewardRateUSDC = _rewardUsdc / rewardsDuration, allowing up to $0.6 worth of precision loss due to USDC's 6 decimals and rewardsDuration being 604800 seconds.

  4. Maximum possible loss is: (60 * 60 * 24 * 7) - 1 = 604799 and average loss can be half of that.

  5. The loss occurs for every notification.

Impact

Stakers suffer a maximum loss of $0.6 in USDC due to precision loss on every reward notification.

The loss function in the worst-case scenario is:

0.6 * No. of time Reward Notified

This means:

PoC

  1. Add the following test_USDCReward_PrecisionLoss_PoC test in StakingRewardsV2.t.sol test.
  2. Run it using command: forge test --match-test test_USDCReward_PrecisionLoss_PoC -vvv

    function test_USDCReward_PrecisionLoss_PoC() public {

        // 0. Setup
        address alice = address(123);
        fundAndApproveAccountV2(alice, TEST_VALUE);

        uint256 initialAliceUsdcBalance = usdc.balanceOf(alice);
        console.log("Initial Alice USDC Balance = ", initialAliceUsdcBalance);

        // 1. Alice stakes
        vm.prank(alice);
        stakingRewardsV2.stake(TEST_VALUE);

        // 2. Reward Notified
        vm.prank(address(rewardsNotifier));
        stakingRewardsV2.notifyRewardAmount(0, 12095000);
        console.log("Total Reward Notified = 12095000");

        // 3. Fast forward 2 weeks
        vm.warp(block.timestamp + 2 weeks);

        // 4. Alice claims reward
        vm.prank(alice);
        stakingRewardsV2.getReward();

        // 5. Conclusion
        uint256 finalAliceUsdcBalance = usdc.balanceOf(alice);
        console.log("final Alice USDC Balance = ", finalAliceUsdcBalance);

        console.log("Loss of Reward for Alice = ", 12095000 - finalAliceUsdcBalance);
    }

Result of the Test:


  Ran 1 test for test/foundry/unit/StakingRewardsV2/StakingRewardsV2.t.sol:StakingRewardsV2Test
  [PASS] test_USDCReward_PrecisionLoss_PoC() (gas: 357680)
  Logs:
    Initial Alice USDC Balance =  0
    Total Reward Notified = 12095000
    final Alice USDC Balance =  11491200
    Loss of Reward for Alice =  603800

  Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.67ms (291.50µs CPU time)

  Ran 1 test suite in 12.31ms (3.67ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Mitigation

Store the rewardRateUSDC scaled by 1e18, reducing the precision loss by a magnitude of 1e12 or implement a recovery function to rescue these USDC tokens.