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: 39/123
Findings: 1
Award: $154.74
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
https://github.com/code-423n4/2023-03-neotokyo/blob/main/contracts/staking/NeoTokyoStaker.sol#L1155 https://github.com/code-423n4/2023-03-neotokyo/blob/main/contracts/staking/NeoTokyoStaker.sol#L1098 https://github.com/code-423n4/2023-03-neotokyo/blob/main/contracts/staking/NeoTokyoStaker.sol#L1627 https://github.com/code-423n4/2023-03-neotokyo/blob/main/contracts/staking/NeoTokyoStaker.sol#L1515 https://github.com/code-423n4/2023-03-neotokyo/blob/main/contracts/staking/NeoTokyoStaker.sol#L1580
When a user stakes or unstakes LP tokens, the following calculation is used. Note the early division by 1e16:
uint256 points = amount * 100 / 1e18 * timelockMultiplier / _DIVISOR;
Due to this division, amounts smaller than 1e16 are not properly considered. This creates various inconsistencies when staking and unstaking.
The problem occurs also when calculating staking points for BYTES.
At the worst case, a user wouldn't be able to withdraw all his staked funds from the contract. He would lose 0.01 LP tokens or 2 BYTES. While not a huge amount, it's not dust either. These are still funds lost for no reason, and obviously not expected behavior from a staking contract.
Additionally, the user will have to manipulate his transaction to withdraw most of his funds - in some scenarios, a simple withdrawal of his whole staked amount will simply revert.
Additionally, a user may get undue rewards by withdrawing 0.01 LP tokens or 2 BYTES at a time, since that will not change his amount of staking points.
These fundamental problems is why I consider this a high severity issue, although understandably it might also be considered medium.
Let's look at how staking points are calculated according to the amount staked. This calculation returns the same staking points for different amounts staked, and has very low granularity (decimals/precision level).
uint256 points = amount * 100 / 1e18 * timelockMultiplier / _DIVISOR;
Due to the division by 1e16, amounts smaller than 1e16 will not contribute to the calculation. I will show the several problems this causes.
Important: please note that the contract saves these points in the user's staking struct, and later tries to deduct the result of this calculation when unstaking.
The following POC shows a scenario where the user can't withdraw all his funds.
User deposits 100e18-1
LPs. According to the abovementioned calculation, he gets 19998
points.
User deposits 100e18-1
LPs again, gets 19998
points.
Total points of the user in the contract - 39996
.
User withdraws 100e18-1 + 1e16+1
LPs. 20002
points are deducted from his balance.
User has 19994
points left in the contract.
If the user tries to redeem his remaining stake - 100e18-1 - (1e16+1)
- due to the inaccurate calculation, the contract would try to deduct 19996
points from the user's point balance
But the user only has 19994
points. So the linked deduction line will revert due to underflow and the user can not withdraw his full balance.
You can use these Solidity functions to see the calculation of the points from these inputs.
This shows how in certain scenarios, a user can not withdraw all his stake.
The user can ask to withdraw 100e18-1 - (2e16)
and succeed. But still the two problems are there:
User has 1e16 tokens stuck in the contract
User needs 2 transactions to unstake his stake
There can be other simpler scenarios for this problem to occur.
For example, depositing 100e18 - 1
twice (getting 19998
points twice) and then withdrawing the whole amount at once (trying to deduct 39998
points) - the withdrawal will revert for same reason - trying to deduct more points than the user has.
So user can't simply withdrawal all his funds in one transaction.
For the same reason - the low granularity - user can get undue rewards.
If for example user deposits 1e18 tokens, then withdraws 1e16-1
, the points calculated would be 0, but the user will still be able to withdraw 1e16-1
tokens. By doing this repeatedly, the user can withdraw his stake, while still keeping the same amount of staking points in the contract. He can also get more rewards by redepositing. The viability of this attack will depend on gas costs and rewards value.
Similar problem occurs when calculating points for BYTES staking:
uint256 bonusPoints = (amount * 100 / (200 * 1e18));
Here the granularity is 2e18, so amounts smaller than that would be lost in calculation. Although the points are deducted only from the total pool and not from each staker's individual position, the lack of granularity is still there, therefore stakers can get undue rewards etc'.
There's an additional small loss of value due to division before multiplication. This manifests mostly as 1 staking point lost. This is a different issue than the one described above. (If this early division is considered a valid issue, please split this issue into that issue as well.) To fix this lacking precision, change the calculation so the division happens only after the multiplication.
amount * timelockMultiplier * 100 / 1e18 / divisor;
Honestly I am not sure and unfortunately do not have enough time to properly consider this, but the options I see are:
#0 - hansfriese
2023-03-16T06:18:45Z
This is a good submission that contains several impacts.
Can not withdraw full stake in one transaction
It explains the same scenario as #450 but doesn't mention getting infinite points instead of reverting because it's inside the unchecked block.User can get undue rewards
It's exactly the same as #348.Note: division before multiplication
It shows the standard rounding problem that is a duplicate of #304.I will consider splitting this one into several issues later.
#1 - c4-judge
2023-03-16T06:19:14Z
hansfriese marked the issue as satisfactory
#2 - c4-judge
2023-03-16T06:19:28Z
hansfriese marked the issue as duplicate of #348
#3 - c4-judge
2023-03-21T09:19:26Z
hansfriese marked the issue as duplicate of #261
#4 - hansfriese
2023-03-21T15:06:17Z
It's all good now as we merged all rounding issues into one.