Platform: Code4rena
Start Date: 23/02/2024
Pot Size: $92,000 USDC
Total HM: 0
Participants: 47
Period: 10 days
Judge: 0xTheC0der
Id: 336
League: ETH
Rank: 6/47
Findings: 1
Award: $5,987.35
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: CodeWasp
Also found by: 0xdice91, 0xlemon, Aamir, Al-Qa-qa, AlexCzm, BAHOZ, Bauchibred, Breeje, DadeKuma, Fassi_Security, PetarTolev, Shield, SpicyMeatball, Trust, ZanyBonzy, cheatc0d3, gesha17, haxatron, imare, jesjupyter, kutugu, lsaudit, marchev, merlinboii, nnez, osmanozdemir1, peanuts, radev_sw, twicek, visualbits
5987.3522 USDC - $5,987.35
https://github.com/code-423n4/2024-02-uniswap-foundation/blob/main/src/UniStaker.sol#L244-L246 https://github.com/code-423n4/2024-02-uniswap-foundation/blob/main/src/UniStaker.sol#L233
Reward tokens are left stuck in UniStaker
contract due to rounding issues.
The protocol rounds down in his favour which is exactly what we want, however that means that there are some tokens left that cannot be claimed. This issue is because of the roundings in UniStaker::rewardPerTokenAccumulated
, UniStaker:: unclaimedReward
and the scaledRewardRate
in UniStaker::notifyRewardAmount
. At first glance it doesn't seem much of a problem, but keep in mind this contracts are not upgradable and don't have a function to claim the left reward tokens. The more stakers there are, the more tokens there are left in the contract. Keep in mind that even though the amount that is left after a round of rewards is insignificant, there will be thousands of rounds even more in the future. This means that the tokens stuck could potentially be worth thousands of dollars.
Add this test in 2024-02-uniswap-foundation\test\UniStaker.t.sol
in the UniStakerTest
contract
To run it: forge test --match-test testStuckTokensAfterRewardDistribution -vv
function testStuckTokensAfterRewardDistribution() public { // reward notifier sends tokens to the unistaker rewardToken.mint(address(this), 100e18); rewardToken.transfer(address(uniStaker), 10e18); vm.prank(rewardNotifier); uniStaker.notifyRewardAmount(10e18); // now users stake their tokens address user1 = makeAddr("user1"); govToken.mint(user1, 10e18); _stake(user1, 10e18, user1); address user2 = makeAddr("user2"); govToken.mint(user2, 10e18); _stake(user2, 10e18, user2); address user3 = makeAddr("user3"); govToken.mint(user3, 10e18); _stake(user3, 10e18, user3); address[] memory users = new address[](100); for(uint256 i = 0; i < 100; i++) { address currentUser = vm.addr(i + 4); govToken.mint(currentUser, 10e18); users[i] = currentUser; _stake(currentUser, 10e18, currentUser); } // Some time passes _jumpAhead(2 days); address newUser = makeAddr("newUser"); govToken.mint(newUser, 10e18); _stake(newUser, 10e18, newUser); // End of reward distribution _jumpAhead(28 days + 1); vm.startPrank(newUser); uniStaker.claimReward(); vm.stopPrank(); vm.startPrank(user1); uniStaker.claimReward(); vm.stopPrank(); vm.startPrank(user2); uniStaker.claimReward(); vm.stopPrank(); vm.startPrank(user3); uniStaker.claimReward(); vm.stopPrank(); for(uint256 i = 0; i < 100; i++) { address currentUser = users[i]; vm.startPrank(currentUser); uniStaker.claimReward(); vm.stopPrank(); } console2.log("Stuck tokens after first distribution (in reward tokens): ", rewardToken.balanceOf(address(uniStaker))); // new reward distribution rewardToken.transfer(address(uniStaker), 10e18); vm.prank(rewardNotifier); uniStaker.notifyRewardAmount(10e18); _jumpAhead(30 days + 1); vm.startPrank(newUser); uniStaker.claimReward(); vm.stopPrank(); vm.startPrank(user1); uniStaker.claimReward(); vm.stopPrank(); vm.startPrank(user2); uniStaker.claimReward(); vm.stopPrank(); vm.startPrank(user3); uniStaker.claimReward(); vm.stopPrank(); for(uint256 i = 0; i < 100; i++) { address currentUser = users[i]; vm.startPrank(currentUser); uniStaker.claimReward(); vm.stopPrank(); } console2.log("Stuck tokens after second distribution (in reward tokens): ", rewardToken.balanceOf(address(uniStaker))); }
We can see after each distribution of tokens how the stuck amount is increased. As I said the amount shown is not that much but over time it would increase a lot. Note that the test is only performed with 100 users, but in reality there will be a lot more users, that means that the tokens stuck will be more since it depends on the users that staked their tokens.
Stuck tokens in UniStaker
Manual Review, VS Code, Foundry
Implement a function that sends this left tokens to the DAO or the users when the amount becomes significant.
Math
#0 - c4-sponsor
2024-03-11T18:02:15Z
apbendi (sponsor) disputed
#1 - apbendi
2024-03-11T18:04:01Z
We don't consider this an issue for several reasons:
#2 - MarioPoneder
2024-03-13T22:28:40Z
Moving forward with QA due to minuscule amounts and existing mitigation measures per protocol design.
#3 - c4-judge
2024-03-13T22:28:47Z
MarioPoneder changed the severity to QA (Quality Assurance)
#4 - c4-judge
2024-03-14T13:31:59Z
MarioPoneder marked the issue as grade-b