UniStaker Infrastructure - 0xlemon's results

Staking infrastructure to empower Uniswap Governance.

General Information

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

Uniswap Foundation

Findings Distribution

Researcher Performance

Rank: 6/47

Findings: 1

Award: $5,987.35

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

5987.3522 USDC - $5,987.35

Labels

bug
downgraded by judge
grade-a
QA (Quality Assurance)
sponsor disputed
:robot:_06_group
Q-16

External Links

Lines of code

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

Vulnerability details

Summary

Reward tokens are left stuck in UniStaker contract due to rounding issues.

Vulnerability Details

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.

Proof of Concept

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.

Impact

Stuck tokens in UniStaker

Tools Used

Manual Review, VS Code, Foundry

Implement a function that sends this left tokens to the DAO or the users when the amount becomes significant.

Assessed type

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:

  1. The amounts involved are truly tiny, even over long periods of time
  2. There is no way to calculate the total extra reward balance onchain, because it requires knowing the claimable rewards of every staker
  3. If the balance ever got so large that it was worth it (unlikely), the admin (i.e. Governance) could choose to make itself a reward notifier and call notifyRewards directly. So a mitigation already exists

#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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter