Core Contracts

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

The increase function can be DOSed

Summary

If a user wants to increase their locked amount but their voting power has decreased too much, the call can revert due to an underflow.

Vulnerability Details

When a user locks a certain amount of RAAC tokens, they have the option to increase their locked amount by calling the increase function (L-251) in the veRAAC contract. This function increases the locked amount, then recomputes the voting power, and finally mints the difference, as shown here:

uint256 newPower = uint256(uint128(newBias));
_checkpointState.writeCheckpoint(msg.sender, newPower);
// Transfer additional tokens and mint veTokens
raacToken.safeTransferFrom(msg.sender, address(this), amount);
_mint(msg.sender, newPower - balanceOf(msg.sender));

The issue is that voting power naturally decreases over time. This means that if the user does not increase their locked amount sufficiently, the newly computed voting power may become smaller than their balance, causing the call to revert.

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 countain 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_increaseUnderflow -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();
}
function test_increaseUnderflow() public {
vm.startPrank(ALICE);
lendingPool.deposit(14069761205393756697566);
stabilityPool.deposit(13624960859879652433378);
vm.warp(block.timestamp+ 7046960);
vm.roll(block.number + uint(7046960)/15);
stabilityPool.withdraw(5621714943215731129846);
veRAAC.lock(18407952740223189400, 115231135);
vm.warp(block.timestamp+ 2688);
vm.roll(block.number + uint(2688)/15);
vm.expectRevert(stdError.arithmeticError);
veRAAC.increase(6524);
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

As a result, the user will be unable to increase his locked amount.

Tools Used

Foundry

Recommendations

The protocol should use an implementation similar to extend

// Update veToken balance
if (newPower > oldPower) {
_mint(msg.sender, newPower - oldPower);
} else if (newPower < oldPower) {
_burn(msg.sender, oldPower - newPower);
}
Updates

Lead Judging Commences

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

veRAACToken::increase underflows on newPower - balanceOf(msg.sender)

Support

FAQs

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