Core Contracts

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

The current implementation of `FeeCollector::claimRewards()` can cause users to lose a significant portion of their rewards due to an incorrect tracking mechanism.

Summary

The current implementation of FeeCollector::claimRewards() can cause users to lose a significant portion of their rewards due to an incorrect tracking mechanism.

Vulnerability Details

The function updates userRewards[user] with totalDistributed to track the rewards a user has claimed:

// totalDistributed Total tokens distributed historically
@> uint256 public totalDistributed;
// FeeCollector::claimRewards()
function claimRewards(address user) external override nonReentrant whenNotPaused returns (uint256) {
if (user == address(0)) revert InvalidAddress();
uint256 pendingReward = _calculatePendingRewards(user);
if (pendingReward == 0) revert InsufficientBalance();
// Reset user rewards before transfer
@> userRewards[user] = totalDistributed;
// Transfer rewards
raacToken.safeTransfer(user, pendingReward);
emit RewardClaimed(user, pendingReward);
return pendingReward;
}

In the _calculatePendingRewards() function, user rewards are determined based on their voting power. However, during subsequent reward claims, share < userRewards[user], causing the transaction to revert and preventing users from claiming additional rewards.

function _calculatePendingRewards(address user) internal view returns (uint256) {
uint256 userVotingPower = veRAACToken.getVotingPower(user);
if (userVotingPower == 0) return 0;
uint256 totalVotingPower = veRAACToken.getTotalVotingPower();
if (totalVotingPower == 0) return 0;
@> uint256 share = (totalDistributed * userVotingPower) / totalVotingPower;
return share > userRewards[user] ? share - userRewards[user] : 0;
}

Poc

Add the following test to test/unit/core/collectors/FeeCollector.test.js and execute it:

describe("claimRewards", function () {
beforeEach(async function () {
await network.provider.send("evm_mine");
// Collect fees first
await feeCollector.connect(user1).collectFee(5000000000000000n, 0);
await feeCollector.connect(user1).collectFee(3000000000000000n, 1);
await feeCollector.connect(user1).collectFee(2000000000000000n, 6);
// Distribute Collect fees first
await feeCollector.connect(owner).distributeCollectedFees();
// cache balance
const user1InitialBalance = await raacToken.balanceOf(user1.address);
const user2InitialBalance = await raacToken.balanceOf(user2.address);
// claim Rewards
await feeCollector.connect(user1).claimRewards(user1.address);
await feeCollector.connect(user2).claimRewards(user2.address);
// cache balance after claim Rewards
const user1BalanceAfterfirstClaimRewards = await raacToken.balanceOf(user1.address);
const user2BalanceAfterfirstClaimRewards = await raacToken.balanceOf(user2.address);
let user1ActualReward = user1BalanceAfterfirstClaimRewards - user1InitialBalance;
let user2ActualReward = user2BalanceAfterfirstClaimRewards - user2InitialBalance;
console.log("user1ActualReward:",user1ActualReward);
console.log("user2ActualReward:",user2ActualReward);
// check userRewards
const user1Reward = await feeCollector.userRewards(user1.address);
const user2Reward = await feeCollector.userRewards(user2.address);
// ❌
console.log("user1Reward:",user1Reward);
console.log("user2Reward:",user2Reward);
});
it("claimRewards revert userReward loss", async function () {
await network.provider.send("evm_mine");
// Collect fees second
await feeCollector.connect(user1).collectFee(5000000000000000n, 0);
await feeCollector.connect(user1).collectFee(3000000000000000n, 1);
await feeCollector.connect(user1).collectFee(2000000000000000n, 6);
await time.increase(WEEK);
await network.provider.send("evm_mine");
// Distribute Collect fees second
await feeCollector.connect(owner).distributeCollectedFees();
// claim Rewards revert ❌
await expect (feeCollector.connect(user1).claimRewards(user1.address)).to.be.revertedWithCustomError(feeCollector, "InsufficientBalance");
await expect (feeCollector.connect(user2).claimRewards(user2.address)).to.be.revertedWithCustomError(feeCollector, "InsufficientBalance");
});
});

output:

FeeCollector
claimRewards
user1ActualReward: 6538810460003384n
user2ActualReward: 3333332170640960n
user1Reward: 10000000000000000n
user2Reward: 10000000000000000n
✔ claimRewards revert userReward loss (93ms)

Note: The actual reward received by the user is significantly smaller than the stored reward value. The second claim attempt fails due to share < userRewards[user].

Impact

The incorrect implementation of the claim function results in users losing a substantial portion of their rewards.

Tools Used

Manual Review

Recommendations

Redesign the implementation of claim rewards

Updates

Lead Judging Commences

inallhonesty Lead Judge 2 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.