Platform: Code4rena
Start Date: 06/01/2023
Pot Size: $210,500 USDC
Total HM: 27
Participants: 73
Period: 14 days
Judge: 0xean
Total Solo HM: 18
Id: 203
League: ETH
Rank: 11/73
Findings: 1
Award: $4,418.17
🌟 Selected for report: 1
🚀 Solo Findings: 1
🌟 Selected for report: Franfran
4418.1679 USDC - $4,418.17
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Furnace.sol#L77-L79 https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSR.sol#L509-L512 https://github.com/reserve-protocol/protocol/blob/946d9b101dd77275c6cbfe0bfe9457927bd221a9/contracts/p1/StRSR.sol#L490-L493
For two instances in the codebase (Furnace
and StRSR
), the composed rewards calculation seems to be wrong.
How the rewards are working in these two snippets is that we are first measuring how much period
or rewardPeriod
occured since the last payout and calculating in only one step the rewards that should be distributed over these periods. In other words, it is composing the ratio over periods.
Taken from the comments, we can write the formula of the next rewards payout as:
with n = (i+1) > 0, n is the number of periods rewards{0} = rsrRewards() payout{i+1} = rewards{i} * payoutRatio rewards{i+1} = rewards{i} - payout{i+1} rewards{i+1} = rewards{i} * (1 - payoutRatio)
Generalization:
It's a geometric mean whose growth rate is (1 - r)
.
Claculation of the sum:
You can play with the graph here.
For a practical example, let's say that our rsrRewardsAtLastPayout
is 5, with a rewardRatio
of 0.9
If we had to calculate our compounded rewards, from the formula given by the comments above, we could calculate manually for the first elements. Let's take the sum for n = 3:
So,
For the values given above, that's
If we do the same calculation with the sum formula
Manual inspection
Rather than dividing by 1 (1e18 from the Fixed library), divide it by the ratio
.
// Furnace.sol // Paying out the ratio r, N times, equals paying out the ratio (1 - (1-r)^N) 1 time. uint192 payoutRatio = FIX_ONE.minus(FIX_ONE.minus(ratio).powu(numPeriods)); uint256 amount = payoutRatio * lastPayoutBal / ratio;
// StRSR.sol uint192 payoutRatio = FIX_ONE - FixLib.powu(FIX_ONE - rewardRatio, numPeriods); // payout: {qRSR} = D18{1} * {qRSR} / r uint256 payout = (payoutRatio * rsrRewardsAtLastPayout) / rewardRatio;
#0 - c4-judge
2023-01-24T16:30:29Z
0xean marked the issue as satisfactory
#1 - c4-sponsor
2023-01-26T18:49:51Z
tbrent marked the issue as sponsor disputed
#2 - tbrent
2023-01-26T18:52:45Z
I think there is a mistake in the math here, possibly arising from the fact that rsrRewards()
doesn't correspond to how much rewards has been handed out, but how much is available to be handed out.
I don't understand why he is computing the sum of u_i
. If u_0
is the value of rsrRewards()
at time 0, and u_1
is the value of rsrRewards()
at time 1, why is the sum of u_i
for all i interesting? This is double-counting balances, since only some of u_i
is handed out each time.
As the number of payouts approach infinity, the total amount handed out approaches u_0
.
#3 - c4-sponsor
2023-01-27T18:04:09Z
tmattimore requested judge review
#4 - 0xean
2023-01-31T15:58:55Z
would be good to get the warden to comment here during QA - will see if we can have the occur to clear up the difference in understanding
#5 - 0xean
2023-02-13T22:18:57Z
I want to apologize that I missed the fact that no response was given during QA and currently believe this issue to be invalid.
#6 - iFrostizz
2023-02-14T07:45:20Z
Hey friends, sorry for not hopping into the discussion earlier ! I'll make sure to subscribe to it next time. My reasoning was that if the staker's rewards doesn't compound over time, then there is no reason for them to stay in the pool and not harvest the rewards, which is a costly process if they would have to harvest each cycle.