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:
_rewardUsdc has precision of 6 as USDC has 6 decimals on Optimism.
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
Alice Stakes some kwenta tokens.
New reward get notified.
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.
Maximum possible loss is: (60 * 60 * 24 * 7) - 1 = 604799 and average loss can be half of that.
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:
The loss will increase linearly with the frequency of reward notifications.
PoC
Add the following test_USDCReward_PrecisionLoss_PoC test in StakingRewardsV2.t.sol test.
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.
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 thenotifyRewardAmount
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:Link to Code
Focus point is formula:
Here,
rewardsDuration
is set to1 week
=604800
._rewardUsdc
has precision of6
as USDC has 6 decimals on Optimism.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
Alice Stakes some kwenta tokens.
New reward get notified.
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 andrewardsDuration
being 604800 seconds.Maximum possible loss is:
(60 * 60 * 24 * 7) - 1
=604799
and average loss can be half of that.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:
This means:
PoC
test_USDCReward_PrecisionLoss_PoC
test inStakingRewardsV2.t.sol
test.forge test --match-test test_USDCReward_PrecisionLoss_PoC -vvv
Result of the Test:
Mitigation
Store the
rewardRateUSDC
scaled by1e18
, reducing the precision loss by a magnitude of1e12
or implement a recovery function to rescue these USDC tokens.