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: 10/123
Findings: 2
Award: $2,849.36
🌟 Selected for report: 0
🚀 Solo Findings: 0
2819.6915 USDC - $2,819.69
In the getPoolReward
function of NeoTokyoStaker.sol
, a user's reward is calculated by first determining totalReward
, which accounts for the total reward emissions since the user's lastPoolRewardTime
.
Then, the user's allocation of the totalReward
is determined by multiplying it by the users stake of pool.totalPoints
.
// Return final shares. unchecked { uint256 share = ((points * _PRECISION) / pool.totalPoints) * totalReward; uint256 daoShare = (share * pool.daoTax) / (100 * _DIVISOR); share /= _PRECISION; daoShare /= _PRECISION; return ((share - daoShare), daoShare); }
This would be appropriate if the total number of points in the pool were constant from the time the user staked until the time getPoolReward
is called. However, changes in the pool.totalPoints
over times creates unfair distributions of points.
To illustrate this consider a simplified use case where there is a single reward window with an emissions rate of 10
per second and a single timelock option of 1000
seconds. Alice and Bob are two participants, each staking an S2 citizen with 10
points.
0
, and withdraws at timestamp 1000
.999
and withdraws at timestamp 1999
.For both of these staking periods 10_000
totalReward
s are emitted and user share is calculated with...
share = ((points * _PRECISION) / pool.totalPoints) * totalReward; share /= _PRECISION;
When Alice's rewards are calculated, pool.totalPoints
is 20
, because of the one overlapping second with Bob's staking period, resulting in her share being 5_000
, even though her stake represented the entire pool for 99.9% of her staking period.
When Bob's rewards are calculated, his share is 10_000
.
So, over the course of the entire scenario, 19_990
rewards we're to be emitted, but only 15_000
were actually rewarded. Additionally, what was rewarded was unfairly distributed between identical stakes.
Manual review
Consider a different method of tracking emissions and rewards such as how MasterChef
's accSushiPerShare
, lastRewardBlock
, and staker rewardDebt
variables allow for a more accurate calculation of rewards.
#0 - c4-judge
2023-03-16T07:23:46Z
hansfriese marked the issue as satisfactory
#1 - c4-judge
2023-03-16T07:24:02Z
hansfriese marked the issue as duplicate of #423
🌟 Selected for report: 0xSmartContract
Also found by: 0x1f8b, 0x6980, 0xAgro, 0xSolus, 0xhacksmithh, 0xkazim, ABA, BPZ, BowTiedOriole, ChainReview, DadeKuma, DeFiHackLabs, Deathstore, DevABDee, Diana, Dravee, Dug, Englave, Go-Langer, Haipls, IceBear, Inspex, Jeiwan, Kek, Kresh, Madalad, MatricksDeCoder, MyFDsYours, RaymondFam, Rolezn, SAAJ, Sathish9098, Taloner, Udsen, Viktor_Cortess, atharvasama, ayden, brgltd, btk, carlitox477, catellatech, chaduke, codeislight, deadrxsezzz, descharre, erictee, fatherOfBlocks, favelanky, glcanvas, handsomegiraffe, jasonxiale, jekapi, joestakey, lemonr, luxartvinsec, martin, matrix_0wl, minhquanym, mrpathfindr, nadin, oyc_109, parsely, peanuts, pfedprog, rbserver, rokso, saian, santipu_, scokaf, slvDev, tsvetanovv, ubl4nk, ulqiorra, yamapyblack, zaskoh
29.6697 USDC - $29.67
In both the stake
and withdraw
functions of NeoTokyoStaker.sol
, the following check exists...
if (uint8(_assetType) == 2 || uint8(_assetType) > 4) { revert InvalidAssetType(uint256(_assetType)); }
This implies that a uint8(_assetType)
value of 4
is valid. However the AssetType
struct is defined as follows...
enum AssetType { S1_CITIZEN, S2_CITIZEN, BYTES, LP }
This results in the InvalidAssetType(uint256 assetType)
error not being returned in all cases where an invalid asset type is provided.
In BYTES2.sol
, the changeStakingContractAddress
and changeTreasuryContractAddress
functions update storage variables without emitting events.
Considering the heavy interdependence between the NeoTokyoStaker.sol
and BYTES2.sol
contracts, it would be prudent to make the STAKER
storage variable immutable or lockable.
#0 - c4-judge
2023-03-16T16:05:40Z
hansfriese marked the issue as grade-c
#1 - c4-judge
2023-03-24T14:31:47Z
hansfriese marked the issue as grade-b