Core Contracts

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

Early distributed gauge rewards are lost when new rewards are notified

Summary

Early distributed gauge rewards are lost when new rewards are notified.

Vulnerability Details

When new gauage rewards are notified by gauge controller, rewardRate is updated by the value returned by notifyReward().

BaseGauge::notifyRewardAmount()

rewardRate = notifyReward(periodState, amount, periodState.emission, getPeriodDuration());

The problem is that notifyReward() calculates rewardRate based only on the current notified amount, this means the early rewards will be lost.

Impact

The early gauge rewards are lost.

POC

- (getRewardPerToken() - userStates[account].rewardPerTokenPaid) / 1e18
+ _balances[msg.sender] * (getRewardPerToken() - userStates[account].rewardPerTokenPaid) / 10000 / 1e18
  • Run forge test --mt testAudit_GaugeRewardLoss.

    // 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_GaugeRewardLoss() public {
    vm.prank(owner);
    gaugeController.addGauge(address(raacGauge), IGaugeController.GaugeType.RAAC, 10000);
    (,,,uint256 periodStartTime) = raacGauge.periodState();
    vm.warp(periodStartTime);
    address alice = makeAddr("Alice");
    uint256 stakingAmount = 100e18;
    deal(address(gaugeStakingToken), alice, stakingAmount);
    // Stake
    vm.startPrank(alice);
    gaugeStakingToken.approve(address(raacGauge), stakingAmount);
    raacGauge.stake(stakingAmount);
    vm.stopPrank();
    deal(address(gaugeRewardToken), address(raacGauge), 30000e18);
    // Distribute rewards twice
    vm.prank(address(gaugeController));
    raacGauge.notifyRewardAmount(10000e18);
    // Distribute rewards
    vm.prank(address(gaugeController));
    raacGauge.notifyRewardAmount(20000e18);
    vm.warp(block.timestamp + raacGauge.getPeriodDuration());
    vm.prank(alice);
    raacGauge.getReward();
    // The total distributed reward amount is 3000, however Alice is only able to claim 2000 by the end of the period
    assertApproxEqAbs(gaugeRewardToken.balanceOf(alice), 20000e18, 1e6);
    }
    }

Tools Used

Manual Review

Recommendations

Add the new reward rate to the existing rate.

- rewardRate = notifyReward(periodState, amount, periodState.emission, getPeriodDuration());
+ rewardRate += notifyReward(periodState, amount, periodState.emission, getPeriodDuration());
Updates

Lead Judging Commences

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

BaseGauge's notifyRewardAmount overwrites reward rates without accounting for undistributed rewards, allowing attackers to reset admin-distributed rewards

Support

FAQs

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

Give us feedback!