This repository has been archived by the owner on Jan 12, 2025. It is now read-only.
PUSH0 - Wrong call order for setTopPoolIdsWithWeights
, resulting in wrong distribution of rewards
#107
Labels
Has Duplicates
A valid issue with 1+ other issues describing the same vulnerability
High
A High severity issue.
Reward
A payout will be made for this issue
Sponsor Confirmed
The sponsor acknowledged this issue is valid
Will Fix
The sponsor confirmed this issue will be fixed
PUSH0
High
Wrong call order for
setTopPoolIdsWithWeights
, resulting in wrong distribution of rewardsSummary
Per the Sherlock rules:
The Masterchef contract allows people to stake an admin-selected token in farms to earn LUM rewards. Each two weeks, MLUM stakers can vote on their favorite pools, and the top pools will earn LUM emissions according to the votes. Admin has to call
setTopPoolIdsWithWeights
to set those votes and weights to set the reward emission for the next two weeks.Per the documented call order for
setTopPoolIdsWithWeights
:We show that this call order is wrong, and will result in wrong rewards distribution.
Vulnerability Detail
There is a global parameter
lumPerSecond
, set by the admin. WheneverupdateAll
is called for a set of pools:totalWeight
weightPid
. This weight can be set by the admin usingsetTopPoolIdsWithWeights
totalLumRewardForPid = (lumPerSecond * weightPid / totalWeight) * (elapsed_time)
, i.e. each second it earnslumPerSecond
times its percentage of voted weightweightPid
across the total weight all top poolstotalWeight
.https://github.com/sherlock-audit/2024-06-magicsea/blob/main/magicsea-staking/src/MasterchefV2.sol#L522-L525
Now, the function
updateAll
does the following:totalLumRewardForPid
since last updateupdateAccDebtPerShare
i.e. distribute rewards since the last updated timehttps://github.com/sherlock-audit/2024-06-magicsea/blob/main/magicsea-staking/src/MasterchefV2.sol#L526-L528
Per the code comments, the admin is responsible for calling
updateAll()
on the old pools before callingsetTopPoolIdsWithWeights()
for the new pools, and then callingupdateAll()
on the new pools.We claim that, using this call order, a pool will be wrongly updated if it's within the set
newPid
but not inoldPid
, and the functions are called with this order. Take this example.PoC
Let LUM per second = 1. We assume all farms were created and registered at time 0:
updateAll
for oldPid. There are no old PidssetTopPoolIdsWithWeights
: Pool A and pool B now have weight = 1.updateAll
for newPid (A and B). 1000 seconds passed, each pool accrued 500 LUM for having 50% weight despite just making it into the top weighted poolsupdateAll
for oldPid (A and B).setTopPoolIdsWithWeights
: Pool A and pool C now have weight = 1.updateAll
for newPid (A and C).The end result is that, at time 2000:
Where the correct result should be:
In total, 3000 LUM has been distributed from timestamps 1000 to 2000, despite the emission rate should be 1 LUM per second. In fact, LUM has been wrongly distributed since timestamp 1000, as both pool A and B never made it into the top pools but still immediately accrued 500 LUM each.
This is because if a pool is included in an
updateAll
call after its weight has been set, its last updated timestamp is still in the past. Therefore whenupdateAll
is called, the new weights are applied across the entire interval since it was last updated (i.e. a far point in the past).Coded PoC
We provide a coded PoC to prove the impact of timestamp 1000. We add two farms A and B. LUM per second is set to 1000 wei per second. We also have a single staker Alice depositing into farm A.
We have two tests to compare the results:
oldPids
, so there's no need to callupdateAll()
on anything before setting weights.updateAll(newPids)
after callingsetTopPoolIdsWithWeights()
.updateAll(newPids)
before callingsetTopPoolIdsWithWeights()
.We output the pending rewards of Alice for comparison.
First, change the function
mint()
of contractMockERC20
to be the following:Then, create a new test file
MasterChefTest.t.sol
:forge test --match-test testSetPoolWeights -vv
And the results are:
As shown, in the "correct" test, Alice has not accrued any rewards right after the new weights are set. However, in the "wrong" test, Alice accrues 499999 reward units right after setting.
Impact
Pools that have just made it into the top pools will have already accrued rewards for time intervals it wasn't in the top pools. Rewards are thus severely inflated.
Code Snippet
https://github.com/sherlock-audit/2024-06-magicsea/blob/main/magicsea-staking/src/Voter.sol#L250-L260
Tool used
Manual Review
Recommendation
All of the pools (oldPids and newPids) should be updated, only then weights should be applied.
In other words, the correct call order should be:
updateAll
should be called for all pools within oldPid or newPid.setTopPoolIdsWithWeights
should then be called.Additionally, we think it might be better that
setTopPoolIdsWithWeights
itself should just callupdateAll
for all (old and new) pools before updating the pool weights, or at least validate that their last updated timestamp is sufficiently fresh.The text was updated successfully, but these errors were encountered: