Core Contracts

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

There can be insufficient gauge rewards for users to claim

Summary

There can be insufficient gauge rewards for users to claim.

Vulnerability Details

When calculates gauage user's earned rewards, their staked amount will be boosted if they are holding veRAAC tokens.

BaseGauge::getUserWeight()

function getUserWeight(address account) public view virtual returns (uint256) {
uint256 baseWeight = _getBaseWeight(account);
@> return _applyBoost(account, baseWeight);
}

And the user's rewards are calculated by the boosted amount.

(Note there is another issue in earn() but it is irrelevant‌ to this issue)

BaseGauge::earned()

function earned(address account) public view returns (uint256) {
@> return (getUserWeight(account) *
(getRewardPerToken() - userStates[account].rewardPerTokenPaid) / 1e18
) + userStates[account].rewards;
}

However, getRewardPerToken() returns reward per token without regard to boosting, as a result, the sum of all users' rewards can be more than the actual reward token balance in the contract, and some users won't be able to claim.

Consider the following scenario:

  1. Alice and Bob stake in the gauge, they both 100 veRAAC tokens;

  2. Gauge controller distributes 10000 reward tokens to gauage;

  3. When Alice claims the her rewards, her staking amount is boosted hence she claims more than 5000 reward tokens;

  4. When Bob claims, he is expected to claim more than 5000 reward tokens, however, there is insufficient funds in the contract, and transaction will revert.

Impact

User won't be able to claim rewards.

POC

- (getRewardPerToken() - userStates[account].rewardPerTokenPaid) / 1e18
+ _balances[msg.sender] * (getRewardPerToken() - userStates[account].rewardPerTokenPaid) / 10000 / 1e18
  • Change as below to fix the issue that base weight boosting issue:

- return (baseWeight * boost) / 1e18;
+ return (baseWeight * boost) / 10000;
  • Run forge test --mt testAudit_GaugeWeightBoost.

// 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_GaugeWeightBoost() public {
vm.prank(owner);
gaugeController.addGauge(address(raacGauge), IGaugeController.GaugeType.RAAC, 10000);
(,,,uint256 periodStartTime) = raacGauge.periodState();
vm.warp(periodStartTime);
address alice = makeAddr("Alice");
address bob = makeAddr("Bob");
// Mint veRAAC tokens
deal(address(raacToken), alice, 100e18);
vm.startPrank(alice);
raacToken.approve(address(veRaacToken), 100e18);
veRaacToken.lock(100e18, 1460 days);
vm.stopPrank();
deal(address(raacToken), bob, 100e18);
vm.startPrank(bob);
raacToken.approve(address(veRaacToken), 100e18);
veRaacToken.lock(100e18, 1460 days);
vm.stopPrank();
uint256 stakingAmount = 100e18;
deal(address(gaugeStakingToken), alice, stakingAmount);
deal(address(gaugeStakingToken), bob, stakingAmount);
// Stake
vm.startPrank(alice);
gaugeStakingToken.approve(address(raacGauge), stakingAmount);
raacGauge.stake(stakingAmount);
vm.stopPrank();
vm.startPrank(bob);
gaugeStakingToken.approve(address(raacGauge), stakingAmount);
raacGauge.stake(stakingAmount);
vm.stopPrank();
// Distribute rewards
deal(address(gaugeRewardToken), address(raacGauge), 10000e18);
vm.prank(address(gaugeController));
raacGauge.notifyRewardAmount(10000e18);
vm.warp(block.timestamp + raacGauge.getPeriodDuration());
vm.prank(alice);
raacGauge.getReward();
// Bob cannot get reward
vm.prank(bob);
vm.expectRevert(IGauge.InsufficientBalance.selector);
raacGauge.getReward();
}
}

Tools Used

Manual Review

Recommendations

The fixing is not trivial, protocol should be aware of each staker's boosted amount, and distribute rewars on a pro rata basis.

Updates

Lead Judging Commences

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

BaseGauge reward calculations divide by 1e18 despite using 1e4 precision weights, causing all user weights to round down to zero and preventing reward distribution

Support

FAQs

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

Give us feedback!