sherlock-audit / 2024-06-magicsea-judging

2 stars 0 forks source link

ChinmayF - Voters will lose all bribe rewards forever if they do not claim their rewards after the last bribing period #164

Open sherlock-admin4 opened 2 months ago

sherlock-admin4 commented 2 months ago

ChinmayF

High

Voters will lose all bribe rewards forever if they do not claim their rewards after the last bribing period

Summary

The claim() function in BribeRewarder is used to claim the rewards associated with a tokenID across all bribe periods that have ended : it iterates over all the voting periods starting from the bribe rewarder's _startVotingPeriod upto the last period that ended according to the Voter.sol contract, and collects and sends rewards to the NFT's owner.

The issue is that if the voter(ie. tokenID owner who earned bribe rewards for one or more bribe periods) does not claim his rewards by the lastVotingPeriod + 2, then all his unclaimed rewards for all periods will be lost forever.

Vulnerability Detail

Lets walk through an example to better understand the issue. Even though the issue occurs in all other cases, we are assuming that the voting period has just started to make it easy to understand.

  1. The first voting period is about to start in the next block. The bribe provider deploys a bribe rewarder and registers it for a pool X for voting periods 1 to 5. ie. the startVotingPeriod in the BribeRewarder.sol = 1 and lastVotingPeriod = 5.
  2. The first voting period starts in voter.sol. Users start voting for pool X and the BribeRewarder keeps getting notified and storing the rewards data in respective rewarder data structure (see here and here)
  3. All 5 voting periods have ended. User voted in all voting periods and got a reward stored for all 5 bribe periods in the BribeRewarder contract. Now when he claims via claim(), he can get all his rewards.
  4. Assume that the 6th voting period has ended. Still if the user calls claim(), he will get back all his rewards. His 6th period rewards will be empty but it does not revert.
  5. Assume that the 7th voting period has ended. Now if the user calls claim(), his call will revert and from now on, he will never be able to claim any of his unclaimed rewards for all periods.

The reason of this issue is this :

    function claim(uint256 tokenId) external override {
        uint256 endPeriod = IVoter(_caller).getLatestFinishedPeriod();
        uint256 totalAmount;

        for (uint256 i = _startVotingPeriod; i <= endPeriod; ++i) {
            totalAmount += _modify(i, tokenId, 0, true);
        }

        emit Claimed(tokenId, _pool(), totalAmount);
    }

The claim function is the only way to claim a user's rewards after he has voted. This iterates over the voting periods starting from the _startVotingPeriod (which is equal to 1 in our example).

This loop's last iteration is the latest voting period that might have ended on the voter contract (regardless of if it was a declared as a bribe period in our own BribeRewarder since voting periods will be a forever going thing and we only want to reward upto a limited set of periods, defined by BribeRewarder:_lastVotingPeriod).

Lets see the Voter.getLatestFinishedPeriod() function :

    function getLatestFinishedPeriod() external view override returns (uint256) {
        if (_votingEnded()) {
            return _currentVotingPeriodId;
        }
        if (_currentVotingPeriodId == 0) revert IVoter__NoFinishedPeriod();
        return _currentVotingPeriodId - 1;
    }

Now if suppose the 6th voting period is running, it will return 5 as finished. if 7th is running, it will return 6, and if 8th period has started, it will return 7.

Now back to claim => _modify. It fetches the rewards data for that period in the _rewards array, which has (lastID - startID) + 2 elements. (see here). In our case, this array will consist of 6 elements (startId = 1 and lastID = 5).

Now when we see how it is fetching the reward data using periodID, it is using the value returned by _indexByPeriodId() as the index of the array.

    function _indexByPeriodId(uint256 periodId) internal view returns (uint256) {
        return periodId - _startVotingPeriod;
    }

So for a periodID = 7, this will return index 6.

Now back to the example above. When the 7th voting period has ended, getLatestFinishedPeriod() will return 7 and the claim function will try to iterate over all the periods that have ended. When the iteration comes to this last period = 7, the _modify function will try to read the array element at index 6 (again we can see this clearly here)

But now it will revert with index out of bounds, because the array only has 6 elements from index 0 to index 5, so trying to access index element 6 in _rewards array will now always revert.

This means that after 2 periods have passed after the last bribing period, no user can ever claim any of their rewards even if they voted for all the periods.

Impact

No user will be able to claim any of their unclaimed rewards for any periods from the BribeRewarder, after this time. The rewards will be lost forever, and a side effect of this is that these rewards will remain stuck in the BribeRewarder. But the main impact is the complete loss of rewards of many users.

High severity because users should always get their deserved rewards, and many users could lose rewards this way at the same time. The damage to the individual user depends on how many periods they didn't claim for and how much amount they used for voting, which could be a very large amount.

Code Snippet

https://github.com/sherlock-audit/2024-06-magicsea/blob/42e799446595c542eff9519353d3becc50cdba63/magicsea-staking/src/rewarders/BribeRewarder.sol#L159

Tool used

Manual Review

Recommendation

The solution is simple : in the claim function, limit the endPeriod used in the loop by the _lastVotingPeriod of a particular BribeRewarder.

    uint256 endPeriod = IVoter(_caller).getLatestFinishedPeriod();
    if (endPeriod > _lastVotingPeriod) endPeriod = _lastVotingPeriod;
0xSmartContract commented 1 month ago

If the function to claim rewards in the BribeRewarder contract is not used within two periods after the last voting period, users will lose all rewards.

This may cause many users to lose their rewards, which will remain locked in the contract forever. Solution ; is to limit the request cycle to _lastVotingPeriod.

0xHans1 commented 1 month ago

PR: https://github.com/metropolis-exchange/magicsea-staking/pull/14

sherlock-admin2 commented 1 month ago

The protocol team fixed this issue in the following PRs/commits: https://github.com/metropolis-exchange/magicsea-staking/pull/12

Reentrants commented 1 month ago

Incorrect duplication, there are a few issues mentioned that should be separated:

  1. Incorrect endPeriod assignment, which this issue elaborates on
  2. Lost bribe rewards whilst there are 0 votes
  3. Rewards can't be swept

They should be separated into 3 different issues, perhaps (2) and (3) can be combined as (3) indirectly solves (2).

slowfinance commented 1 month ago

I am afraid Reentrants is right.

chinmay-farkya commented 1 month ago

Escalate

This set of issues is actually two completely different sets of issues clubbed together incorrectly. One set is the “Voters will lose all bribe rewards forever if they do not claim their rewards after the last bribing period” ie. this issue #164 which emphasizes the loss of legit rewards already earned by the voters. In this case, the root cause and fix are different ie. voters will not be able to claim their rewards after certain time. In this case, the voters are affected.

Here are duplicates of #164 : Set 1 : #145, #501, #495, Note that three issues #591, #161, #371 also identify the same issue but the description of these issues is technically incorrect because they say that the array out-of-bound revert will start happening after the lastVotingPeriod +1 which is not true because the array actually gets a length of lastID - startId + 2, as an extra empty element is added to the rewards array due to the wrong(but non-impactful) logic here. This means that while these three issues say revert will happen after lastId +1 period, the revert will actually only start happening after lastId + 2 periods as per the current logic. Thus, these three issues should be deemed invalid due to the incorrect description of the issue, as they could not identify the correct conditions in which the bug manifests.

Thus, set 1 has only four valid issues : #164, #145, #501, #495

While the other set emphasises on the rewards that were not earned by any voter (maybe due to zero votes etc.) and thus they remain stuck in the briber contract. In this set, the root cause is the non-existence of a sweep function to restore rewards that were ‘’’not earned by anyone’. In this case, the briber itself is affected.

Solving one of them doesn’t solve the other as the fixes and root cause are completely unrelated.

Set 2 :

94, #121, #135, #139, #172, #180, #183, #255, #263, #278, #291, #298, #315, #317, #374, #408, #417, #470, #473, #486, #586, #652

Additional :

257 and #216 are duplicates of #52

110 is not a duplicate and is actually invalid as it does not correctly identify the root cause and instead talks about out-of-gas due to loop which is impossible given the block gas limit of iota is 1B units as seen here https://explorer.evm.iota.org/block/424441.

543 is definitely not a duplicate and actually invalid because the amount has been already scaled using “Constants.ACC_PRECISION_BITS” in the mentioned code.

Both set 1 and set 2 are still high severity issues.

sherlock-admin3 commented 1 month ago

Escalate

This set of issues is actually two completely different sets of issues clubbed together incorrectly. One set is the “Voters will lose all bribe rewards forever if they do not claim their rewards after the last bribing period” ie. this issue #164 which emphasizes the loss of legit rewards already earned by the voters. In this case, the root cause and fix are different ie. voters will not be able to claim their rewards after certain time. In this case, the voters are affected.

Here are duplicates of #164 : Set 1 : #145, #501, #495, Note that three issues #591, #161, #371 also identify the same issue but the description of these issues is technically incorrect because they say that the array out-of-bound revert will start happening after the lastVotingPeriod +1 which is not true because the array actually gets a length of lastID - startId + 2, as an extra empty element is added to the rewards array due to the wrong(but non-impactful) logic here. This means that while these three issues say revert will happen after lastId +1 period, the revert will actually only start happening after lastId + 2 periods as per the current logic. Thus, these three issues should be deemed invalid due to the incorrect description of the issue, as they could not identify the correct conditions in which the bug manifests.

Thus, set 1 has only four valid issues : #164, #145, #501, #495

While the other set emphasises on the rewards that were not earned by any voter (maybe due to zero votes etc.) and thus they remain stuck in the briber contract. In this set, the root cause is the non-existence of a sweep function to restore rewards that were ‘’’not earned by anyone’. In this case, the briber itself is affected.

Solving one of them doesn’t solve the other as the fixes and root cause are completely unrelated.

Set 2 :

94, #121, #135, #139, #172, #180, #183, #255, #263, #278, #291, #298, #315, #317, #374, #408, #417, #470, #473, #486, #586, #652

Additional :

257 and #216 are duplicates of #52

110 is not a duplicate and is actually invalid as it does not correctly identify the root cause and instead talks about out-of-gas due to loop which is impossible given the block gas limit of iota is 1B units as seen here https://explorer.evm.iota.org/block/424441.

543 is definitely not a duplicate and actually invalid because the amount has been already scaled using “Constants.ACC_PRECISION_BITS” in the mentioned code.

Both set 1 and set 2 are still high severity issues.

You've created a valid escalation!

To remove the escalation from consideration: Delete your comment.

You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.

Honour-d-dev commented 1 month ago

Escalate

This set of issues is actually two completely different sets of issues clubbed together incorrectly. One set is the “Voters will lose all bribe rewards forever if they do not claim their rewards after the last bribing period” ie. this issue #164 which emphasizes the loss of legit rewards already earned by the voters. In this case, the root cause and fix are different ie. voters will not be able to claim their rewards after certain time. In this case, the voters are affected.

Here are duplicates of #164 : Set 1 : #145, #501, #495, Note that three issues #591, #161, #371 also identify the same issue but the description of these issues is technically incorrect because they say that the array out-of-bound revert will start happening after the lastVotingPeriod +1 which is not true because the array actually gets a length of lastID - startId + 2, as an extra empty element is added to the rewards array due to the wrong(but non-impactful) logic here. This means that while these three issues say revert will happen after lastId +1 period, the revert will actually only start happening after lastId + 2 periods as per the current logic. Thus, these three issues should be deemed invalid due to the incorrect description of the issue, as they could not identify the correct conditions in which the bug manifests.

Thus, set 1 has only four valid issues : #164, #145, #501, #495

While the other set emphasises on the rewards that were not earned by any voter (maybe due to zero votes etc.) and thus they remain stuck in the briber contract. In this set, the root cause is the non-existence of a sweep function to restore rewards that were ‘’’not earned by anyone’. In this case, the briber itself is affected.

Solving one of them doesn’t solve the other as the fixes and root cause are completely unrelated.

Set 2 : #94, #121, #135, #139, #172, #180, #183, #255, #263, #278, #291, #298, #315, #317, #374, #408, #417, #470, #473, #486, #586, #652

Additional : #257 and #216 are duplicates of #52 #110 is not a duplicate and is actually invalid as it does not correctly identify the root cause and instead talks about out-of-gas due to loop which is impossible given the block gas limit of iota is 1B units as seen here https://explorer.evm.iota.org/block/424441.

543 is definitely not a duplicate and actually invalid because the amount has been already scaled using “Constants.ACC_PRECISION_BITS” in the mentioned code.

Both set 1 and set 2 are still high severity issues.

set 1 (duplicates of #164) is invalid , using the docs as a source of truth image rewards not claimed on the period after bribe ends should not be claimable by voters ,and should be left in contract to be swept by the briber. I believe the extra period empty period added in the _rewards array is for this purpose (ie to give the voters an extra period to claim their rewards)

0xSmartContract commented 1 month ago

Escalate ; 2 different set . (I do not aggre )

The reward mechanism in the BribeRewarder contract should be considered as a whole. Both users losing their earned rewards (#164) and unclaimed rewards getting stuck in the contract (#94) are different aspects of the same system. If these problems are solved separately, the integrity of the system can be compromised and unexpected interactions can occur.

Both problems are fundamentally due to deficiencies in the reward management logic of BribeRewarder. For example, the indexing logic in the _modify() function and the limitations of the claim() function contribute to both problems.

Changes made to solve one problem can affect the other. For example, a change made to ensure that users can always claim their rewards can also affect the management of unclaimed rewards

chinmay-farkya commented 1 month ago

If these problems are solved separately, the integrity of the system can be compromised

I absolutely disagree that dupes are decided based on arriving at a common broader solution for two problems that are separate in the logic. That doesn't matter in issue duplication/separation on sherlock imo.

Honour-d-dev commented 1 month ago

@0xSmartContract set 1 of this escalation are actually invalid as mentioned in my previous comment. Voters are intentionally allowed only one extra period to claim their rewards. After which rewards are locked to be claimed by the briber.

If this is not the case then there will be conflict as to when the briber is allowed to sweep the contract for unclaimed rewards. hence the need for the deadline.

Slavchew commented 1 month ago

If these problems are solved separately, the integrity of the system can be compromised

I absolutely disagree that dupes are decided based on arriving at a common broader solution for two problems that are separate in the logic. That doesn't matter in issue duplication/separation on sherlock imo.

I agree with this even more so that both issues have different fixes, locations, and impacts. The fact that both are associated with rewards does not make them one. If that were the case, there are over 5 award-related issues in the entire audit, should they be grouped together, NO of course.

WangSecurity commented 1 month ago

I've hidden one comment cause it seemed to be repeating the same point and not adding value.

About this issue, I agree that duplicates are:

145, #501, #495, #591.

591 - says Claiming rewards for any period after _lastVotingPeriod + 2 will be impossible and not after _lastVotingPeriod + 2 as said in the escalation comment, so I assume it's correct, excuse me if wrong, please correct.

371 and #161 agree to be incorrect.

But, @Honour-d-dev comment is very interesting, is there any evidence that this behaviour is not intended?

Additional : https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/257 and https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/216 are duplicates of https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/52 https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/110 is not a duplicate and is actually invalid as it does not correctly identify the root cause and instead talks about out-of-gas due to loop which is impossible given the block gas limit of iota is 1B units as seen here https://explorer.evm.iota.org/block/424441. https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/543 is definitely not a duplicate and actually invalid because the amount has been already scaled using “Constants.ACC_PRECISION_BITS” in the mentioned code.

Completely agree with the part quoted above and plan to apply these changes.

Additionally, I agree that the second set mentioned in the escalation comment is a different issue. The root causes, fixes and vulnerability paths are different. Even though the impact is similar, it's not a ground to duplicate all of them into 1 issue. The best will be #172, the duplicates are: https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/94, https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/121, https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/135, https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/139, https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/172, https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/180, https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/183, https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/255, https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/263, https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/278, https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/291, https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/298, https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/315, https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/317, https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/374, https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/408, https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/417, https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/470, https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/473, https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/486, https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/586, https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/652

Waiting for an answer about lost rewards after 2 epochs before making the decision.

Slavchew commented 1 month ago

The @Honour-d-dev comment comes from just one piece of information in the docs, here's the full text:

https://docs.magicsea.finance/protocol/magic/magic-lum-voting#bribes

image

As far as I understand, this is just to say that the bribe rewards for the current period can be claimed after it ends.

"Voters can claim the rewards until the next epoch is ended."

The statement above - rewards not claimed on the period after bribe ends should not be claimable by voters - https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/164#issuecomment-2265233078 is not true as it's never states in the docs that bribe rewards can only be claimed 1 period after the end and no more, it states that Voters can claim the rewards until the next epoch is ended, not the end one.

Honour-d-dev commented 1 month ago

From my understanding Until means "not after" , no?

The statement in the docs: "Voters can claim the rewards until the next epoch is ended"

Is saying , "if the last bribe epoch for the BribeRewarder is epoch-3, then voters have until the end of epoch-4 (i.e. the next epoch) to claim rewards"

This makes even more sense ,considering the issue with sweep functionality so there needs to be a point where rewards not yet claimed can be considered as forfeit and the briber is allowed to sweep them.

@Slavchew I don't see how the full context of the docs disproves this

chinmay-farkya commented 1 month ago

Hey @WangSecurity this response is regarding the comment by honour-d-dev here : https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/164#issuecomment-2267602192

I believe that the docs, especially the relevant section, is outdated.

image

Here is the evidence for the docs being outdated.

  1. The issue #346 says that "rewards can be claimed immediately after the epoch ends" which is non-impactful issue and thus invalid. The sponsor added a Won't fix label to that because the intention was not what the docs stated (the same section of docs relevant here), what makes sense is to allow reward claiming after the epoch, and that is what the code says.

  2. Voters can claim the rewards until the next epoch is ended

If this statement in the docs was to be true then the claim function in BribeRewarder would have allowed only the one current finished period in the claim logic. See here.

The claim function forces to loop over all the finished periods(and not just the latest one) which means that the code was intended to allow staker to claim his rewards for all periods that have ended, and not just the one that ended latest.

So the code was really "meant to allow stakers to claim their rewards for all finished periods" otherwise the claim function would be having just the "getLatestFinishedPeriod()" in the claiming logic.

About #591 : yes, my bad it is a duplicate. But see that #145 is not a duplicate (this one says it will revert after lastVotingPeriod + 1 which is not true). I misnumbered the issue in the original escalation.

TLDR, #164 shall remain a separate valid high severity issue with duplicates : #501, #495, #591

Honour-d-dev commented 1 month ago

@chinmay-farkya

  1. I do not agree that sponsor adding won't fix tag is evidence that the docs is outdated , same docs was provided for the audit as relevant protocol resources, no?. #346 just has no impact.

If this statement in the docs was to be true then the claim function in BribeRewarder would have allowed only the one current finished period in the claim logic. See here.

The claim function forces to loop over all the finished periods(and not just the latest one) which means that the code was intended to allow staker to claim his rewards for all periods that have ended, and not just the one that ended latest.

I do not understand the argument here, if you're questioning why the bribe rewards for other periods (not the last one) are still claimable until the end of the last period+1 , then the answer is similar to #346, it's not impactful if the voter can still claim rewards for earlier periods as long as the BribeRewarder is still active.

It is Impactful ,however, if the voter can still claim rewards after the BribeRewarder is no longer active as this will interfere with the sweeping of unclaimed or undistributed tokens. If the voter is given unlimited time to claim rewards then at what point will it be safe to send back unclaimed rewards to the briber as stated by the docs, without the voters that has not claimed yet loosing their rewards? image

chinmay-farkya commented 1 month ago

I do not understand the argument here, if you're questioning why the bribe rewards for other periods (not the last one) are still claimable until the end of the last period+1 , then the answer is similar to https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/346, it's not impactful if the voter can still claim rewards for earlier periods as long as the BribeRewarder is still active.

I'm saying that if we were to believe what the docs say, then it should not have been encoded into the claim function to loop over a set of epochs. if "rewards could be claimed only until the next epoch ended" : If this would have been true and intended then the claim function would have the _modify call for only one epoch ie. "getLatestFinishedPeriod()" : ie. after epoch 1 ends, claim should call _modify for only 1st epoch and the loop would not have been in place at all. And that is not the case, so the docs do not portray what the code is designed for.

The current claim function logic means that the rewards claim has to be allowed for all finished periods uptil now, and not just the "only one latest finished period" {that is what the docs say}. Code takes precedence over the docs.

So in short, the claim has been designed to allow claiming all periods at once as per code (and code > docs), and rest you can read the original bug description.

It is Impactful ,however, if the voter can still claim rewards after the BribeRewarder is no longer active as this will interfere with the sweeping of unclaimed or undistributed tokens.

In this context also, the docs are actually outdated/not updated acc to what the code is designed for currently. There was no sweep function in BribeRewarder at all. Sweeping out rewards earned by users also does not make sense, they should be able to claim them if they have been accrued. The bribeRewarder can only be used once, so the unclaimed funds can sit without any problems.

The current recommendation around "briberewarder should have a sweep function" pointed out in other bug is for "undistributed rewards" and not for "unclaimed rewards", because sweeping out funds which were not even accrued to anybody due to no votes in an apoch(for ex.) makes sense, but sweeping out rewards earned by users and still sitting in the briberewarder as unclaimed does not make sense.

If the voter is given unlimited time to claim rewards then at what point will it be safe to send back unclaimed rewards to the briber

They should never be sent back, only the undistributed rewards shall be. That can be easily done by recording all accrued rewards in a separate variable lets say "Rewardsdistributed" and only sweeping out balance - rewardsdistributed back to the protocol. Also see thats how it is done in the BaseRewarder here : maintaining a reserve variable.

Slavchew commented 1 month ago

@WangSecurity Instead of trying to descramble the words in the documentation, can't the sponsor just come and write if they think it's okay for users to lose bribe rewards if they haven't taken them max 2 periods after that? Everyone will agree that it is not acceptable, because the rewards are not sent and everyone has to claim them and not everyone would have the opportunity to claim them during this period.

I think that will settle the whole escalation.

Honour-d-dev commented 1 month ago

I'm saying that if we were to believe what the docs say, then it should not have been encoded into the claim function to loop over a set of epochs. if "rewards could be claimed only until the next epoch ended" : If this would have been true and intended then the claim function would have the _modify call for only one epoch ie. "getLatestFinishedPeriod()" : ie. after epoch 1 ends, claim should call _modify for only 1st epoch and the loop would not have been in place at all. And that is not the case, so the docs do not portray what the code is designed for.

So in short, the claim has been designed to allow claiming all periods at once as per code (and code > docs), and rest you can read the original bug description.

Looping over all epochs to calculate rewards is a design decision (as opposed to having the voter specify the exact epoch they voted/have rewards in) it does not contradict or invalidate the docs.

In this context also, the docs are actually outdated/not updated acc to what the code is designed for currently. There was no sweep function in BribeRewarder at all. Sweeping out rewards earned by users also does not make sense, they should be able to claim them if they have been accrued. The bribeRewarder can only be used once, so the unclaimed funds can sit without any problems.

Again, docs is not outdated , BribeRewarder can be used multiple times actually ( "The bribeRewarder can only be used once" is false) and sweeping unclaimed rewards makes sense as it is in the protocol docs(which makes it public information) so voters would be aware of it and know that they have a 14-day window to claim rewards

They should never be sent back, only the undistributed rewards shall be. That can be easily done by recording all accrued rewards in a separate variable lets say "Rewardsdistributed" and only sweeping out balance - rewardsdistributed back to the protocol. Also see thats how it is done in the BaseRewarder here : maintaining a reserve variable.

It's not possible to record all the rewards accrued by users in this implementation of the masterChef algo because rewards still accrue(or is distributed) even when nobody votes(as distribution is time-based i.e. emissionsPerSecond, to encourage voters to vote as early as possible) and a voter's specific reward is dependent on the time they voted and the total votes in that epoch. The _reserve variable in BaseRewarder is actually the total rewards available in the contract and the _totalUnclaimedRewards works a differently there because there's no concept of epochs that reset the total supply to 0, just a start and end time. Hence why Briberewarder is not BaseRewarder

chinmay-farkya commented 1 month ago

Looping over all epochs to calculate rewards is a design decision (as opposed to having the voter specify the exact epoch they voted/have rewards in) it does not contradict or invalidate the docs.

That is completely false. That does invalidate the docs. The docs say that only the latest period shall be allowed to be claimed, but the code loops over all periods including the latest one which means it is meant to allow claiming for all past periods, how are these two equivalent statements ? They are clearly not. So the code needs to be believed and the code has a bug ie. #164

BribeRewarder can be used multiple times actually ( "The bribeRewarder can only be used once" is false)

Again that is completely false. here is a surprise for you : the briberewarder can only be initialized once : see https://github.com/sherlock-audit/2024-06-magicsea/blob/42e799446595c542eff9519353d3becc50cdba63/magicsea-staking/src/rewarders/BribeRewarder.sol#L227

It's not possible to record all the rewards accrued by users in this implementation of the masterChef algo because rewards still accrue(or is distributed) even when nobody votes

Again completely false the rewards would get stuck if no one voted thats what the other set of issues in another bug is. Please read #172

lets leave baserewarder out of the current discussion an focus on the current bug.

Honour-d-dev commented 1 month ago

The docs say that only the latest period shall be allowed to be claimed,

The docs does not say or imply this please. This is a false statement

Again that is completely false. here is a surprise for you : the briberewarder can only be initialized once

In original comment https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/164#issuecomment-2284552877 you said used not initialized,

The bribeRewarder can only be used once, so the unclaimed funds can sit without any problems.

i interpreted as used to claim rewards (which can be done multiple times)

Again completely false the rewards would get stuck if no one voted thats what the other set of issues in another bug is.

I did not say rewards don't get stuck, i said rewards are still accrued if no one votes. For example if the BribeRewarder is to distribute 100usd to voters for that epoch, if the first voter votes after 7 days(half-way into the epoch) then they can only earn a maximum of 50usd assuming no one else votes on the bribed pool. Which means rewards still accrue regardless of votes or not (but to nobody if there's no votes). i can provide POC of this if you're still in doubt.

WangSecurity commented 1 month ago

Instead of trying to descramble the words in the documentation, can't the sponsor just come and write if they think it's okay for users to lose bribe rewards if they haven't taken them max 2 periods after that? Everyone will agree that it is not acceptable, because the rewards are not sent and everyone has to claim them and not everyone would have the opportunity to claim them during this period

I would like to do that, but the sponsor is OOS for 2 weeks, so we cannot get this information.

But I believe we have sufficient arguments to settle the discussion. The docs say:

Voters can claim the rewards until the next epoch is ended.

In this case, if the last epoch is 5 (arbitrary number), then we shouldn't be able to claim rewards after the 6th epoch has ended. But, as I understand and as this report shows, the rewards can be claimed after the 6th epoch has ended, but cannot be claimed after the 7th epoch. If I'm correct here, then we have contextual evidence that the docs are outdated.

Then the question about not having the sweep function for the briber to get back the rewards. In that sense, it's still a valid bug, since not all rewards can be distributed, so they should be swept by the briber.

Hope that answers the questions and I believe it's a fair decision here. About #145, agree it shouldn't be a duplicate. Hence, my decision is to accept the escalation, apply the changes expressed here, except #145 being invalid. Both families will have High severity.

iamnmt commented 1 month ago

@WangSecurity @chinmay-farkya

I believe #145 stating the correct condition in which the bug occurs

when Voter#getLatestFinishedPeriod() is greater than BribeRewarder#_lastVotingPeriod + 1

To elaborate on this, Voter#getLatestFinishedPeriod() is greater than BribeRewarder#_lastVotingPeriod + 1 if and only if (_currentVotingPeriodId is equal to BribeRewarder#_lastVotingPeriod + 2 and current voting period is ended) or _currentVotingPeriodId is greater than BribeRewarder#_lastVotingPeriod + 2

https://github.com/sherlock-audit/2024-06-magicsea/blob/42e799446595c542eff9519353d3becc50cdba63/magicsea-staking/src/Voter.sol#L471

I do not see anything wrong with this condition. Please correct me if I am wrong.

This condition is the same as "after 2 periods have passed after the last bribing period".

But see that https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/145 is not a duplicate (this one says it will revert after lastVotingPeriod + 1 which is not true)

Nowhere in #145 mention that it will revert after lastVotingPeriod + 1.

WangSecurity commented 1 month ago

Users can not claim from BribeRewarder when Voter#getLatestFinishedPeriod() is greater than BribeRewarder#_lastVotingPeriod + 1, which will result in DoS on BribeRewarder#claim and loss of funds

Here's the impact from #145. It says the rewards will be unable to be claimed after _lastVotingPeriod + 1. The rewards will be unable to be claimed because the function will revert. But, that's not fully correct, it's still possible to claim the rewards after _lastVotingPeriod + 1 during _lastVotingPeriod + 2. That's why I believe the report is not sufficient because rewards become unable to be claimed after _lastVotingPeriod + 2 when the report says +1. Hence, I believe it's incorrect.

The decision remains as expressed here

iamnmt commented 1 month ago

@WangSecurity @chinmay-farkya

it's still possible to claim the rewards after _lastVotingPeriod + 1 during _lastVotingPeriod + 2

You are correct.

It says the rewards will be unable to be claimed after _lastVotingPeriod + 1

No, it is not. #145 intentionally mentions the function Voter#getLatestFinishedPeriod()

Voter#getLatestFinishedPeriod() is greater than BribeRewarder#_lastVotingPeriod + 1

If the current voting period is _lastVotingPeriod + 2 and the voting period is not over, then Voter#getLatestFinishedPeriod() will return _lastVotingPeriod + 1. If the current voting period is _lastVotingPeriod + 2 and the voting period is over, then Voter#getLatestFinishedPeriod() will return _lastVotingPeriod + 2. That is why I believe this condition is the same as "after 2 periods have passed after the last bribing period".

Another way to look at this condition is just looking at BribeRewarder#claim in isolation. There are _lastVotingPeriod - _startVotingPeriod + 2 elements in the _rewards array, meaning the array can be indexed up to _lastVotingPeriod + 1. Indexing an element greater than _lastVotingPeriod + 1 will cause revert. When BribeRewarder#claim is called, the Voter#getLatestFinishedPeriod()th element will be indexed. Thus, if Voter#getLatestFinishedPeriod() is greater than _lastVotingPeriod + 1, then the function call will revert.

Because of the reasons above, I believe the condition in #145 is correct. I kindly request you to review your decision again.

chinmay-farkya commented 1 month ago

Yeah I relooked at it, and now I see you are right.

145 is a correct duplicate of this. "getLatestFinishedPeriod > lastVotingPeriod + 1" essentially means the same thing as "last finsihed period = lastVotingPeriod + 2"

Sorry got confused earlier with the wording.

WangSecurity commented 1 month ago

Excuse me, @iamnmt is correct and I didn't notice this. Planning to accept the escalation and apply the changes as here making 2 families with High severity.

PeterSRWeb3 commented 4 weeks ago

@WangSecurity @0xSmartContract This issue should be a duplicate https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/409 of the second family of issues, because it's pointing out the same problem and it's proposing the same solution. Thanks in advance!

WangSecurity commented 4 weeks ago

As I understand #409 report is incorrect. The example given is that the claiming of rewards will revert when the latest finished voting period is 5 if the rewards are distributed from 2 to 4 periods. But, as I understand this is incorrect and the rewards can still be claimed until 6th voting period is finished, based on the discussion above and this report.

Simply put, the #409 report says the claiming of rewards is impossible after _latestVotingPeriod + 1, when in fact it's still possible and reverts when _latestVotingPeriod + 2. Correct me if my understanding of #409 is incorrect.

The decision remains as expressed here

PeterSRWeb3 commented 4 weeks ago

@WangSecurity Thanks sir for taking a look! This is due to the fact that I've ran the test using the first reccomendation. Of course this is my mistake, but the report is valid for me, because it's pointing the same issue and it's proposing the same solution. Please take a look again sir. I totally understand you and I will do my best, to not make this type of mistakes again.

WangSecurity commented 3 weeks ago

Still, the report is incorrect, hence, my decision remains the same.

PeterSRWeb3 commented 3 weeks ago

Okay, sir, my bad in this situation. Thanks for the effort

WangSecurity commented 3 weeks ago

Result: High Has duplicates

sherlock-admin4 commented 3 weeks ago

Escalations have been resolved successfully!

Escalation status:

sherlock-admin2 commented 2 weeks ago

The Lead Senior Watson signed off on the fix.