Platform: Code4rena
Start Date: 08/03/2023
Pot Size: $60,500 USDC
Total HM: 2
Participants: 123
Period: 7 days
Judge: hansfriese
Id: 220
League: ETH
Rank: 6/123
Findings: 2
Award: $2,974.43
🌟 Selected for report: 0
🚀 Solo Findings: 0
2819.6915 USDC - $2,819.69
It calculates the staking reward wrongly so users will receive wrong amount of reward.
It calculates the staking reward like MasterChef of SusiSwap but it's wrongly implemented.
Current one calculates the reward with the totalPoints
of the pool and it's wrong.
User A
staked 100 points at Time 0
. totalPoints
of the pool is 100 and reward rate is 1 now.Time 100
, his reward will be rewardRate * timeSpan * userPoints / totalPoints = 1 * 100 * 100 / 100 = 100
User B
staked another 100 points at Time 100
. totalPoints
will be 200 now.User A
claims the reward at Time 101
, it will be rewardRate * timeSpan * userPoints / totalPoints = 1 * 101 * 100 / 200 = 50.5
.totalPoints
was increased by User B
before claiming the reward.So the reward amount will be calculated wrongly according to the totalPoints
.
Manual Review
We should create and track accumulatedRewardPerPoint
like MasterChef of SushiSwap and calculate each user's reward accordingly.
#0 - c4-judge
2023-03-16T03:00:03Z
hansfriese marked the issue as satisfactory
#1 - c4-judge
2023-03-16T03:00:45Z
hansfriese marked the issue as duplicate of #423
🌟 Selected for report: rokso
Also found by: 0x52, 0xnev, ABA, BPZ, DadeKuma, Haipls, J4de, Jeiwan, Josiah, Krace, LegendFenGuin, Lirios, MadWookie, RaymondFam, Ruhum, Toshii, UdarTeam, aashar, ak1, anodaram, auditor0517, carlitox477, cccz, jekapi, juancito, kaden, kenzo, minhquanym, nobody2018, rbserver, rokso, ulqiorra
154.74 USDC - $154.74
Users can manipulate their points and steal most of the staking reward.
When users stake and withdraw LP
, it calculates the points like this.
File: NeoTokyoStaker._stakeLP() 1155: uint256 points = amount * 100 / 1e18 * timelockMultiplier / _DIVISOR;
File: NeoTokyoStaker._withdrawLP() 1622: unchecked { 1623: uint256 points = amount * 100 / 1e18 * lpPosition.multiplier / _DIVISOR; 1624: 1625: // Update the caller's LP token stake. 1626: lpPosition.amount -= amount; 1627: lpPosition.points -= points; 1628: 1629: // Update the pool point weights for rewards. 1630: pool.totalPoints -= points; 1631: }
There is a rounding during the point calculation and an attacker can inflate her points easily by staking 2 times(so rounding 2 times) and withdrawing at a time.
Alice
stakes 1e16 + 1
of LP
when timelockMultiplier = 100
. So her points will be 1.Alice
stakes 1e16 - 1
of LP
again. Her points will be 1 anyway due to the rounding in _stakeLP()
.Alice
withdraws 2e16
of LP
at a time and the points at L1623 will be 2 and lpPosition.points = 1 - 2 = -1
so her points will be type(uint256).max
because it's inside the unchecked
block.Manual Review
We should check one condition during the subtraction.
unchecked { uint256 points = amount * 100 / 1e18 * lpPosition.multiplier / _DIVISOR; // max cap if(points > lpPosition.points) { points = lpPosition.points; } // Update the caller's LP token stake. lpPosition.amount -= amount; lpPosition.points -= points; // Update the pool point weights for rewards. pool.totalPoints -= points; }
#0 - c4-judge
2023-03-16T03:02:15Z
hansfriese marked the issue as duplicate of #450
#1 - c4-judge
2023-03-16T03:02:20Z
hansfriese marked the issue as satisfactory
#2 - c4-judge
2023-03-17T01:09:15Z
hansfriese marked the issue as duplicate of #261
🌟 Selected for report: rokso
Also found by: 0x52, 0xnev, ABA, BPZ, DadeKuma, Haipls, J4de, Jeiwan, Josiah, Krace, LegendFenGuin, Lirios, MadWookie, RaymondFam, Ruhum, Toshii, UdarTeam, aashar, ak1, anodaram, auditor0517, carlitox477, cccz, jekapi, juancito, kaden, kenzo, minhquanym, nobody2018, rbserver, rokso, ulqiorra
154.74 USDC - $154.74
Dust points would remain in the totalPoints
after withdrawing all LP
When users stake and withdraw LP
, it calculates the points like this.
File: NeoTokyoStaker._stakeLP() 1155: uint256 points = amount * 100 / 1e18 * timelockMultiplier / _DIVISOR;
File: NeoTokyoStaker._withdrawLP() 1622: unchecked { 1623: uint256 points = amount * 100 / 1e18 * lpPosition.multiplier / _DIVISOR; 1624: 1625: // Update the caller's LP token stake. 1626: lpPosition.amount -= amount; 1627: lpPosition.points -= points; 1628: 1629: // Update the pool point weights for rewards. 1630: pool.totalPoints -= points; 1631: }
Due to the rounding, some points would remain after the withdrawal.
Alice
staked 1e16
of LP
and got 1 point when timelockMultiplier = 100
.5e15
twice and her points will be 1 due to the rounding. Also, totalPoints
wasn't decreased.LP
and other users will receive less amount of reward.It's a minor amount but it should be fixed for users' preferences.
Manual Review
When we compare the current staking logic and ERC4626 vault
, we may say LP
is asset
, and points
is shares
.
Then _withdrawLP()
should calculate the points like ERC4626.previewWithdraw().
So we should round up during the points calculation and apply the max cap according to my other submission - Users can inflate their points during LP staking
.
#0 - c4-judge
2023-03-16T09:37:45Z
hansfriese marked the issue as satisfactory
#1 - c4-judge
2023-03-16T09:38:00Z
hansfriese marked the issue as duplicate of #348
#2 - c4-judge
2023-03-21T09:20:32Z
hansfriese marked the issue as duplicate of #261
#3 - c4-judge
2023-03-29T00:18:59Z
hansfriese marked the issue as not a duplicate
#4 - c4-judge
2023-03-29T00:19:18Z
hansfriese changed the severity to 3 (High Risk)
#5 - c4-judge
2023-03-29T00:19:52Z
hansfriese marked the issue as duplicate of #261