Core Contracts

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

Denial-of-Service (DoS) vulnerability exists in the `claimRewards` function

Summary

A Denial-of-Service (DoS) vulnerability exists in the claimRewards function of feeCollector.sol. When users claim their rewards, the contract resets their accumulated rewards in a way that prevents them from claiming any newly accumulated rewards in subsequent Claiming rounds. This miscalculation causes users to receive zero rewards after the first round of reward distribution, effectively locking their funds.

2025-02-raac/contracts/core/collectors/FeeCollector.sol at main · Cyfrin/2025-02-raac

Vulnerability Details

Issue Description:
The function resets the user's reward balance by setting:

userRewards[user] = totalDistributed;

before transferring the rewards. The calculation for pending rewards is then performed in the internal call of

2025-02-raac/contracts/core/collectors/FeeCollector.sol at main · Cyfrin/2025-02-raac

_calculatePendingRewards() function, which uses:

return share > userRewards[user] ? share - userRewards[user] : 0;

After the initial claim, userRewards[user] is equal to totalDistributed, causing the function to always return 0 for any subsequent claims, even when additional fees have been collected and distributed. This flaw leads to a DoS condition where users are unable to claim rewards beyond the first round.

Proof of Concept (PoC):

Add this to the FeeCollector.test.js

it.only("Should Revert when users try claim rewards in Second round of Fee Distribution", async function () {
const taxRate = SWAP_TAX_RATE + BURN_TAX_RATE; // 1.5%
const grossMultiplier = BigInt(BASIS_POINTS * BASIS_POINTS) / BigInt(BASIS_POINTS * BASIS_POINTS - taxRate * BASIS_POINTS);
const protocolFeeGross = ethers.parseEther("50") * grossMultiplier / BigInt(10000);
const lendingFeeGross = ethers.parseEther("30") * grossMultiplier / BigInt(10000);
const swapTaxGross = ethers.parseEther("20") * grossMultiplier / BigInt(10000);
// Collect fees from users
await feeCollector.connect(user1).collectFee(protocolFeeGross, 0);
await feeCollector.connect(user1).collectFee(lendingFeeGross, 1);
await feeCollector.connect(user1).collectFee(swapTaxGross, 6);
await feeCollector.connect(user2).collectFee(protocolFeeGross, 0);
await feeCollector.connect(user2).collectFee(lendingFeeGross, 1);
await feeCollector.connect(user2).collectFee(swapTaxGross, 6);
// Create token locks
await veRAACToken.connect(user1).lock(ethers.parseEther("1000"), ONE_YEAR);
await veRAACToken.connect(user2).lock(ethers.parseEther("500"), ONE_YEAR);
await time.increase(WEEK);
// First fee distribution and reward claims
await feeCollector.connect(owner).distributeCollectedFees();
await time.increase(WEEK);
await feeCollector.connect(user1).claimRewards(user1.address);
await feeCollector.connect(user2).claimRewards(user2.address);
// Second round of fee collection and distribution
await feeCollector.connect(user1).collectFee(protocolFeeGross, 0);
await feeCollector.connect(user1).collectFee(lendingFeeGross, 1);
await feeCollector.connect(user1).collectFee(swapTaxGross, 6);
await feeCollector.connect(user2).collectFee(protocolFeeGross, 0);
await feeCollector.connect(user2).collectFee(lendingFeeGross, 1);
await feeCollector.connect(user2).collectFee(swapTaxGross, 6);
// Additional locks affecting share calculations
await veRAACToken.connect(user1).lock(ethers.parseEther("1000"), ONE_YEAR);
await veRAACToken.connect(user2).lock(ethers.parseEther("500"), ONE_YEAR);
await time.increase(WEEK);
// Second fee Distribution and reward claims
await feeCollector.connect(owner).distributeCollectedFees();
// The claim should revert due to insufficient calculated rewards
await expect(feeCollector.connect(user1).claimRewards(user1.address)).to.be.revertedWithCustomError(feeCollector, "InsufficientBalance");
});

Impact

Accumulated fees may remain unclaimed indefinitely, potentially causing significant issues in reward distribution.

Users are unable to claim any rewards in rounds following their first claim, resulting in locked funds and a loss of rewards.

Tools Used

Manual Review and Hardhat

Recommendations


Instead of resetting the user's reward balance to totalDistributed, adjust the logic to properly account for rewards already claimed. For example, update the balance to reflect only the portion of rewards that has been claimed, leaving pending rewards intact for future claims or reset all users rewards claimed before distributing.

Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

FeeCollector::claimRewards sets `userRewards[user]` to `totalDistributed` seriously grieving users from rewards

Support

FAQs

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