The FeeCollector
contract's reward distribution uses a flawed cumulative tracking system that prevents users from claiming new rewards from new distributions after their initial claim. This occurs in the claimRewards
function due to absolute total tracking instead of per-distribution accounting. The key risk is protocol funds becoming permanently unclaimable despite being distributed
The reward calculation uses a single totalDistributed
accumulator and stores absolute claimed amounts rather than tracking per-distribution rewards.when ever a user who have a voting power claims rewards , it stores his userRewards
to the current totalDistributed
value :
The flaw originates from how future reward calculations then subtract this stored value, effectively “locking in” the cumulative amount from the first claim and preventing any additional rewards from being claimed by that user when his not the only one in the system .
The protocol continuously collects fees, so totalDistributed
increases over time (for example: 100 → 200 → 300 RAAC).
Rewards pending for a user are calculated using the formula:
```solidity
pendingRewards = (currentTotalDistributed * userPower) / totalPower - userRewards[user];
```
Here, userRewards[user]
is updated to the current totalDistributed
whenever the user claims rewards.
To simplify the issue consider the following example:
Initial State:
totalDistributed = 100
User A has voting power of 50 out of a total of 100.
By formula: User A’s share = (100 × 50/100) = 50 tokens.
When User A claims rewards, userRewards[A]
is set to 100.
After New Distribution:
The protocol adds 100 more tokens, so totalDistributed
becomes 200.
User A’s share (if calculated freshly) would be: (200 × 50/100) = 100 tokens.
However, since userRewards[A]
is still 100, the new pending rewards become: 100 − 100 = 0.
Result: Despite User A maintaining the same voting power, they cannot claim any additional rewards from the new distribution.
Non-Cumulative Claims: Once a user claims rewards, their userRewards
value locks them out of receiving any future rewards, even though new funds are added to the pool.
Lost Rewards: Newly distributed fees become unclaimable by existing users because the claim calculation incorrectly assumes that the user’s share has been fully compensated.
Root Cause: The system uses an absolute cumulative total (totalDistributed
) and updates userRewards[user]
to match it at claim time rather than tracking each distribution period separately. This means that after the first claim, any increases in totalDistributed
are effectively ignored for that user.
Note: If a user is the sole holder of voting power, the calculation works as expected, since the entire distribution is attributed to that user.
i'm using foundry for test , to integrate foundry :
run :
add this to hardhat.config.cjs
:
run :
comment the test/unit/libraries/ReserveLibraryMock.sol
as it's causing compiling errors
inside test folder , create new dir foundry
and inside it , create new file baseTest.sol
, and copy/paste this there :
now create a pocs.sol
inside test/foundry
, and copy/paste this there :
the following PoC shows how user1 and user2 wasn't able to claim their rewards after the first distribution cycle due to the flowed formula :
Rewards become unclaimable after first distribution cycle
Protocol accumulates dead funds with each distribution
High severity: Direct fund loss + protocol functionality failure
foundry , manual review
Implementing checkpoint-based tracking—for example, by recording each distribution separately and allowing users to claim their share of each epoch—would preserve historical distribution data and enable correct cumulative claims.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.