Core Contracts

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

Gauge controller don't decrease the gauge weight correctly

Summary

The update of gauge weight does not take into account changes in voting power, making the computation of the new weight incorrect or even causing the call to vote to revert due to an underflow.

Vulnerability Details

When a veRAAC token holder wants to vote for a gauge, the Gauge Controller updates the gauge weight by calling the internal function _updateGaugeWeight. This function removes the weight set by the user, scaled by the user’s current voting power.

function _updateGaugeWeight(
address gauge,
uint256 oldWeight,
uint256 newWeight,
uint256 votingPower
) internal {
Gauge storage g = gauges[gauge];
uint256 oldGaugeWeight = g.weight;
uint256 newGaugeWeight = oldGaugeWeight - (oldWeight * votingPower / WEIGHT_PRECISION)
+ (newWeight * votingPower / WEIGHT_PRECISION);
g.weight = newGaugeWeight;
g.lastUpdateTime = block.timestamp;
}

The issue is that the protocol uses the current voting power without considering any changes that may have occurred in the user’s voting power.

For instance, if a user increases their locked amount and then changes their vote by voting again, the protocol will overestimate the amount to remove.

Example:

1. A user has a voting power of 10,000 and votes for a gauge, assigning it 5,000 weight. As a result, the gauge receives a weight of 5,000.

2. The user then increases their locked amount by calling the increase function (L-251 in veRAACToken), raising their voting power to 20,000.

3. When the user decides to change the weight of the gauge and calls vote, the function will attempt to remove 10,000 weight, which exceeds the previously assigned weight of 5,000.

In order to run this coded POC you will have to add foundry to the project to do so you will have to follow 4 steps :

run npm install --save-dev @nomicfoundation/hardhat-foundry

add require("@nomicfoundation/hardhat-foundry"); to the hardhat.config file

run npx hardhat init-foundry

remove The ReserveLibraryMock from the test folder because this file countains errors that will make the compilation imposible for foundry.

you can now copy paste this code in a solidity file in the test folder and

run forge test --mt test_GaugeWeigthPOC -vv.

// SPDX-License-Identifier: GPL-2.0
pragma solidity ^0.8.0;
import {Test, console,stdError} from "forge-std/Test.sol";
import {crvUSDToken} from "contracts/mocks/core/tokens/crvUSDToken.sol";
import {RAACToken} from "contracts/core/tokens/RAACToken.sol";
import {veRAACToken} from "contracts/core/tokens/veRAACToken.sol";
import {RAACReleaseOrchestrator} from "contracts/core/minters/RAACReleaseOrchestrator/RAACReleaseOrchestrator.sol";
import {RAACHousePrices} from "contracts/core/primitives/RAACHousePrices.sol";
import {RAACNFT} from "contracts/core/tokens/RAACNFT.sol";
import {RToken} from "contracts/core/tokens/RToken.sol";
import {DebtToken} from "contracts/core/tokens/DebtToken.sol";
import {LendingPool, ILendingPool} from "contracts/core/pools/LendingPool/LendingPool.sol";
import {Treasury} from "contracts/core/collectors/Treasury.sol";
import {FeeCollector} from "contracts/core/collectors/FeeCollector.sol";
import {DEToken} from "contracts/core/tokens/DEToken.sol";
import {StabilityPool} from "contracts/core/pools/StabilityPool/StabilityPool.sol";
import {RAACMinter} from "contracts/core/minters/RAACMinter/RAACMinter.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import {Auction} from "contracts/zeno/Auction.sol";
import {ZENO} from "contracts/zeno/ZENO.sol";
import {ERC20Mock} from "contracts/mocks/core/tokens/ERC20Mock.sol";
import {TimelockController} from "contracts/core/governance/proposals//TimelockController.sol";
import {Governance} from "contracts/core/governance/proposals//Governance.sol";
import {RAACGauge} from "contracts/core/governance/gauges/RAACGauge.sol";
import {RWAGauge} from "contracts/core/governance/gauges/RWAGauge.sol";
import {BoostController} from "contracts/core/governance/boost/BoostController.sol";
import {GaugeController} from "contracts/core/governance/gauges/GaugeController.sol";
import {IGaugeController} from "contracts/interfaces/core/governance/gauges/IGaugeController.sol";
import {IGovernance} from "contracts/interfaces/core/governance/proposals/IGovernance.sol";
import {ReserveLibrary} from "contracts/libraries/pools/ReserveLibrary.sol";
import {LockManager} from "contracts/libraries/governance/LockManager.sol";
import {VotingPowerLib} from "contracts/libraries/governance/VotingPowerLib.sol";
import {TimeWeightedAverage} from "contracts/libraries/math/TimeWeightedAverage.sol";
import "contracts/libraries/math/WadRayMath.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract CodedPOC is Test{
using WadRayMath for uint256;
address constant BOB = address(0x10000);
address constant ALICE = address(0x20000);
address constant CHARLIE = address(0x30000);
address[] internal users;
address owner = makeAddr("Owner");
address[] public proposers = [makeAddr("Proposers")];
address[] public executors = [makeAddr("Executors")];
// protocol contracts
crvUSDToken crvUSD;
RAACToken raac;
veRAACToken veRAAC;
RAACReleaseOrchestrator raacReleaseOrchestrator;
RAACHousePrices raacHousePrices;
RAACNFT raacNFT;
RToken rToken;
DebtToken debtToken;
LendingPool lendingPool;
Treasury treasury;
FeeCollector feeCollector;
DEToken deToken;
StabilityPool stabilityPool;
RAACMinter raacMinter;
Treasury repairFund;
//ghost variables;
CurveMock curveMock;
ERC20Mock usdc;
ERC20Mock rewardToken;
ZENO currentZeno;
ZENO[] zenos;
Auction currentAuction;
Auction[] auctions;
TimelockController timelock;
Governance governance;
GaugeController gaugeController;
RAACGauge raacGauge;
RWAGauge rwaGauge;
BoostController boostController;
uint256[] proposalIds;
address[] gauges;
uint256 timestampBefore;
function setUp() public {
vm.warp(1524785992);
vm.roll(4370000);
users = [BOB, ALICE, CHARLIE];
vm.label(BOB, "Bob");
vm.label(ALICE, "Alice");
vm.label(CHARLIE, "Charlie");
vm.label(owner, "Owner");
vm.label(address(this), "Test");
crvUSD = new crvUSDToken(owner);
curveMock = new CurveMock(address(crvUSD));
vm.label(address(curveMock), "CurveMock");
vm.prank(owner);
crvUSD.setMinter(address(this));
raac = new RAACToken(owner, 200, 50);
vm.label(address(raac), "RAAC");
veRAAC = new veRAACToken(address(raac));
vm.label(address(veRAAC), "veRAAC");
raacReleaseOrchestrator = new RAACReleaseOrchestrator(owner);
vm.label(address(raacReleaseOrchestrator), "RAACReleaseOrchestrator");
raacHousePrices = new RAACHousePrices(owner);
raacNFT = new RAACNFT(address(crvUSD), address(raacHousePrices), owner);
rToken = new RToken("RToken", "RT", owner, address(crvUSD));
debtToken = new DebtToken("DebtToken", "DT", owner);
lendingPool = new LendingPool(
address(crvUSD), address(rToken), address(debtToken), address(raacNFT), address(raacHousePrices), 0.1e27
);
lendingPool.setPrimeRateOracle(address(owner));
//lendingPool.setCurveVault(address(curveMock));
treasury = new Treasury(address(owner));
repairFund = new Treasury(address(owner));
feeCollector =
new FeeCollector(address(raac), address(veRAAC), address(treasury), address(repairFund), address(owner));
deToken = new DEToken("DEToken", "DET", owner, address(rToken));
stabilityPool = new StabilityPool(address(owner));
raacMinter = new RAACMinter(address(raac), address(stabilityPool), address(lendingPool), address(treasury));
vm.prank(owner);
raac.setFeeCollector(address(feeCollector));
vm.prank(owner);
raac.manageWhitelist(address(feeCollector), true);
vm.prank(owner);
raac.manageWhitelist(address(veRAAC), true);
vm.prank(owner);
raac.manageWhitelist(address(owner), true);
vm.prank(owner);
raac.setMinter(owner);
// raac.mint(BOB, 1_000_000e18);
// raac.mint(ALICE, 1_000_000e18);
// raac.mint(CHARLIE, 1_000_000e18);
usdc = new ERC20Mock("USDC", "USDC");
vm.label(address(usdc), "USDC");
vm.prank(owner);
raac.setMinter(address(raacMinter));
bytes32 role = feeCollector.FEE_MANAGER_ROLE();
vm.prank(owner);
feeCollector.grantRole(role, owner);
role = feeCollector.EMERGENCY_ROLE();
vm.prank(owner);
feeCollector.grantRole(role, owner);
role = feeCollector.DISTRIBUTOR_ROLE();
vm.prank(owner);
feeCollector.grantRole(role, owner);
vm.prank(owner);
rToken.setReservePool(address(lendingPool));
vm.prank(owner);
debtToken.setReservePool(address(lendingPool));
vm.prank(owner);
deToken.setStabilityPool(address(stabilityPool));
vm.prank(owner);
// Set up minter configuration
raac.transferOwnership(address(raacMinter));
vm.prank(owner);
rToken.transferOwnership(address(lendingPool));
vm.prank(owner);
debtToken.transferOwnership(address(lendingPool));
vm.prank(owner);
stabilityPool.initialize(
address(rToken), address(deToken), address(raac), address(raacMinter), address(crvUSD), address(lendingPool)
);
// setValueContractAddress = address(new SetValueContract());
// Set up lending pool configuration
lendingPool.setStabilityPool(address(stabilityPool));
vm.prank(owner);
raacHousePrices.setOracle(owner);
vm.prank(owner);
stabilityPool.setRAACMinter(address(raacMinter));
_createAuction();
rewardToken = new ERC20Mock("RewardToken", "RT");
timelock = new TimelockController(2 days, proposers, executors, owner);
governance = new Governance(address(veRAAC), address(timelock));
role = timelock.PROPOSER_ROLE();
vm.prank(owner);
timelock.grantRole(role, address(governance));
role = timelock.EXECUTOR_ROLE();
vm.prank(owner);
timelock.grantRole(role, address(governance));
role = timelock.CANCELLER_ROLE();
vm.prank(owner);
timelock.grantRole(role, address(governance));
gaugeController = new GaugeController(address(veRAAC));
raacGauge = new RAACGauge(address(rewardToken), address(raac), address(gaugeController));
vm.label(address(raacGauge), "RAACGauge");
gaugeController.addGauge(address(raacGauge), IGaugeController.GaugeType.RAAC, 10000);
raacGauge.grantRole(raacGauge.CONTROLLER_ROLE(), owner);
raacGauge.grantRole(raacGauge.FEE_ADMIN(), address(owner));
raacGauge.grantRole(raacGauge.EMERGENCY_ADMIN(), address(owner));
vm.prank(owner);
raacGauge.setBoostParameters(25000, 10000, 1 weeks);
vm.prank(owner);
raacGauge.setDistributionCap(10000000e18);
vm.prank(owner);
// Set initial weight after time alignment
raacGauge.setInitialWeight(5000); // 50% weight
rwaGauge = new RWAGauge(address(rewardToken), address(raac), address(gaugeController));
vm.label(address(rwaGauge), "RWAGauge");
gaugeController.addGauge(address(rwaGauge), IGaugeController.GaugeType.RWA, 10000);
rwaGauge.grantRole(rwaGauge.CONTROLLER_ROLE(), address(owner));
rwaGauge.grantRole(rwaGauge.FEE_ADMIN(), address(owner));
rwaGauge.grantRole(rwaGauge.EMERGENCY_ADMIN(), address(owner));
// rewardToken.mint(address(raacGauge), type(uint256).max / 2);
//rewardToken.mint(address(rwaGauge), type(uint256).max / 2);
// Initialize boost parameters
vm.prank(owner);
rwaGauge.setBoostParameters(
25000, // maxBoost (2.5x = 25000 basis points)
10000, // minBoost (1x = 10000 basis points)
7 * 24 * 3600 // boostWindow (7 days)
);
vm.prank(owner);
// Set distribution cap
rwaGauge.setDistributionCap(10000000e18);
vm.prank(owner);
// Set monthly emission
rwaGauge.setMonthlyEmission(100000e18);
boostController = new BoostController(address(veRAAC));
boostController.modifySupportedPool(address(lendingPool), true);
gauges = [address(raacGauge), address(rwaGauge)];
for (uint256 i; i < users.length; i++) {
crvUSD.mint(users[i], 1_000_000_000e18);
vm.prank(users[i]);
crvUSD.approve(address(raacNFT), type(uint256).max);
vm.prank(users[i]);
crvUSD.approve(address(lendingPool), type(uint256).max);
vm.prank(users[i]);
debtToken.approve(address(lendingPool), type(uint256).max);
vm.prank(users[i]);
rToken.approve(address(stabilityPool), type(uint256).max);
vm.prank(users[i]);
raac.approve(address(veRAAC), type(uint256).max);
usdc.mint(users[i], 1_000_000_000e18);
vm.prank(users[i]);
raac.approve(address(raacGauge), type(uint256).max);
vm.prank(users[i]);
raac.approve(address(rwaGauge), type(uint256).max);
}
timestampBefore = block.timestamp;
}
function _createAuction() internal {
currentZeno = new ZENO(address(usdc), 365 days, "ZENO", "ZN", owner);
currentAuction = new Auction(
address(currentZeno),
address(usdc),
address(currentZeno),
block.timestamp + 1 days,
block.timestamp + 11 days,
500,
10,
1000e18,
owner
);
vm.prank(owner);
currentZeno.transferOwnership(address(currentAuction));
for (uint256 i; i < users.length; i++) {
vm.prank(users[i]);
usdc.approve(address(currentAuction), type(uint256).max);
}
auctions.push(currentAuction);
zenos.push(currentZeno);
}
function test_lockTwoTimePOC() public {
vm.startPrank(CHARLIE);
//charlie deposit in the lending and stability pool
lendingPool.deposit(10_000e18);
stabilityPool.deposit(rToken.balanceOf(CHARLIE));
vm.warp(block.timestamp+ 6003);
vm.roll(block.number + uint256(6003)/15);
stabilityPool.withdraw(deToken.balanceOf(CHARLIE));
veRAAC.lock(raac.balanceOf(CHARLIE)/2,31554267);
uint256 balanceOfCharlie = veRAAC.balanceOf(CHARLIE);
uint256 charlieVotingPower = veRAAC.getVotingPower(CHARLIE);
assertEq(balanceOfCharlie, charlieVotingPower);
veRAAC.lock(raac.balanceOf(CHARLIE)/2,31537156);
vm.stopPrank();
balanceOfCharlie = veRAAC.balanceOf(CHARLIE);
charlieVotingPower = veRAAC.getVotingPower(CHARLIE);
assertEq(balanceOfCharlie, charlieVotingPower);
}
function test_UserHaveDebtAfterLiquidation() public {
// 350 days
vm.warp(block.timestamp+30299853);
vm.roll(block.number + 2271488);
vm.prank(owner);
raacHousePrices.setHousePrice(1, 653482530205154129803885);
vm.startPrank(BOB);
//house of 653482 dollars
raacNFT.mint(1, 653482530205154129803885);
raacNFT.approve(address(lendingPool),1);
// 69 days
vm.warp(block.timestamp+6027005);
vm.roll(block.number + 2058436);
lendingPool.deposit(425197589667951795675698);
// 288 days
vm.warp(block.timestamp+24960365);
vm.roll(block.number + 111874);
lendingPool.depositNFT(1);
// 13 days
vm.warp(block.timestamp+1148672);
vm.roll(block.number + 653211);
lendingPool.borrow(345941753031933799366395);
vm.stopPrank();
// 4 hours
vm.warp(block.timestamp+14784);
vm.roll(block.number + 1474);
lendingPool.setParameter(ILendingPool.OwnerParameter.LiquidationThreshold,1044);
// 62 days
vm.warp(block.timestamp+5437407);
vm.roll(block.number + 1619098);
vm.prank(ALICE);
lendingPool.initiateLiquidation(BOB);
// 140 days
vm.warp(block.timestamp+140 days);
vm.roll(block.number + uint(2961535)/140);
uint256 userDebt ;
uint256 scaledUserDebt;
uint256 beforeState = vm.snapshot();
lendingPool.updateState();
//We compute the debt after the call of update state
userDebt = lendingPool.getUserDebt(BOB);
console.log("User debt after the state update: %d",userDebt);
vm.revertTo(beforeState);
userDebt = lendingPool.getUserDebt(BOB);
console.log("User debt without updating the state: %d",userDebt);
scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
crvUSD.mint(address(stabilityPool), scaledUserDebt);
vm.expectRevert();
vm.prank(owner);
stabilityPool.liquidateBorrower(BOB);
}
function test_GaugeWeigthPOC() public {
vm.startPrank(BOB);
lendingPool.deposit(697106689045178731767167);
vm.warp(block.timestamp + 8801775);
vm.roll(block.number + 1931936);
stabilityPool.deposit(414308523880797109194139);
stabilityPool.withdraw(1702830198);
veRAAC.lock(15290, 31544587) ;
gaugeController.vote(gauges[0], 7174);
veRAAC.increase(43310635545591792929782);
vm.expectRevert(stdError.arithmeticError);
gaugeController.vote(gauges[0], 5362);
vm.stopPrank();
}
}
contract CurveMock is ERC4626 {
IERC20 curveToken;
constructor(address token) ERC4626(IERC20(token)) ERC20("curve vault", "CV") {}
function withdraw(uint256 assets, address receiver, address owner, uint256 maxLoss, address[] calldata strategies)
external
returns (uint256 shares) {
withdraw(assets, receiver, owner);
}
}

Impact

This miscalculation can cause unexpected behavior, including incorrect weight adjustments or transaction failures due to underflow.

Tools Used

Medusa.

Recommendations

The protocol should change the _updateGaugeWeight to take into account the changes of the voting power.

Updates

Lead Judging Commences

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

GaugeController::_updateGaugeWeight uses current voting power for both old and new vote calculations, causing underflows when voting power increases and incorrect gauge weights

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

GaugeController::_updateGaugeWeight uses current voting power for both old and new vote calculations, causing underflows when voting power increases and incorrect gauge weights

Support

FAQs

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

Give us feedback!