Open c4-bot-5 opened 6 months ago
The issue is well demonstrated, properly formatted, contains a coded POC. Marking as HQ.
0xSorryNotSorry marked the issue as high quality report
0xSorryNotSorry marked the issue as primary issue
Acknowledging this, disagree with severity (imo it's informational).
This is the expected behavior, users are supposed to check each other and if the rewards go up, claimRewards of others so that they are not earning more rewards unduly. And if rewards are going down (nominal foreseen case), users are expected to claim their rewards prior to the rewardRatio decline. Ultimately the governance is a game of who has relatively more tokens, so the users act as keepers to each other to make sure no undue rewards are earned. Also there is no interpolation of the rewards because the amounts are expected to be small and frequent.
eswak (sponsor) acknowledged
eswak marked the issue as disagree with severity
Trumpero changed the severity to QA (Quality Assurance)
Trumpero marked the issue as grade-a
Considering this issue as informational
Hey, @Trumpero, thanks for your time,
We believe this issue should be reconsidered because the sponsor explained their approach and how the protocol should function, but we demonstrate that it's completely incorrect.
We highlight two main problems with the rewards.
When the rewardRatio changes, the guild rewards up to that point should be calculated based on the old ratio. After the change, new rewards should be accumulated based on the updated ratio. However, the current implementation doesn't support this behavior.
The sponsor suggested that users should monitor if the rewardRatio
changes and then execute claimRewards
for themselves and all other users in each term within the current market.
If the rewardRatio
changes and there are 100 stakers, according to the suggestion, the subsequent 100 transactions would require all stakers to execute claimRewards
, which is deemed impractical and hardly to achieve. Additionally, there's a concern about the user's incentive to claim rewards if the ratio drops, as all their rewards, regardless of the staked amount, will be based on the new ratio.
Looking at the other scenario, it's evident that rewards are solely based on the amount staked, not on the duration of staking. This means if I stake at the beginning and someone stakes 100 blocks after me, during reward distribution, both will receive equal amounts, eliminating the entire incentive for staking early.
Here's a similar problem example: https://solodit.xyz/issues/m-11-should-accrue-before-change-loss-of-rewards-in-case-of-change-of-settings-code4rena-reserve-reserve-contest-git
@Slavchew
Reward distribution is intended to be a game among users, where users follow and interact with each other. This is the reason why claimRewards
and applyGaugeLoss
functions are permissionless. They don't need to update all positions, instead, they should consider the completed positions of others and the gas cost to act and optimize their rewards. Additionally, the likelihood of changes in rewardRatio and mintRatio is low, so the cost to update other positions is not too much compared to the long-term reward.
Lines of code
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/loan/SurplusGuildMinter.sol#L216-L290 https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/loan/SurplusGuildMinter.sol#L328-L333
Vulnerability details
Impact
Every staker in the SurplusGuildMinter accumulates rewards for their Guild tokens received upon staking. The reward calculation relies on the Guild amount for each staker converted to Credit reward and the current reward ratio. However, there is an issue with how rewards are handled after the update of the reward ratio.
When the
rewardRatio
changes, the guild rewards up to that point should be calculated based on the old ratio. After the change, new rewards should be accumulated based on the updated ratio. However, the current implementation doesn't support this behavior. Instead, regardless of how therewardRatio
changes, it always multiplies it with the staker's guild amount converted to credit reward. This is because, on arewardRatio
update, the system does not store all staker rewards up to that moment.See: Line 252
The issue lies in the fact that, irrespective of the rewardRatio being 10%, 8%, or 6%, the computation takes place only when the
getRewards()
function is called. This poses a problem because if a staker has maintained their stake from the beginning and callsgetRewards()
later when the ratio is, for instance, 6%, they won't receive the correct amount of rewards but the calculation will calculate it as if it was 6% from the beginning.Also, if staker A stakes at time 0 and claims rewards at a 6% rate at time 1500, and staker B stakes at time 1499 and claims rewards with staker A at time 1500, both will receive the same amount. This is totally wrong and remove the incentive for staking.
Proof of Concept 1
getRewards()
, the rewardRatio is 6%, resulting in an incorrect payout from 2000 guild (in the form of credit rewards) * 6%. This is inaccurate as the staker joined when the reward ratio was 10%.Results from the diagram provided above and test.
Coded PoC
Import
console
from"@forge-std/Test.sol”
Comment out these 2 lines in the
setUp()
to work on an empty balance and see it more clearly:Place the 3 tests in
SurplusGuildMinter.t.sol
Run them with:
Proof of Concept 2
getRewards()
.Coded PoC
Import
console
from"@forge-std/Test.sol”
Comment out these 2 lines in the
setUp()
to work on an empty balance and see it more clearly:Place the test in
SurplusGuildMinter.t.sol
Run with:
Tools Used
Manual Review
Recommended Mitigation Steps
When the Governor calls
setRewardRatio()
, it should first invokegetRewards()
for all stakers. Moreover, it is crucial to implement an elapsed time logic for calculating rewards based on the duration elapsed from onerewardRatio
to another. Each update of therewardRatio
should be treated as a distinct phase, and all rewards for these phases should be stored in the UserStake struct. The calculation for phase rewards should be executed each timesetRewardRatio()
is called, considering the time elapsed since the last update.Assessed type
Context