Core Contracts

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

Incorrect Token Minting Logic in `veRAACToken.increase()` Prevent Users From Increasing Their Locks

Summary

The increase() function in veRAACToken incorrectly attempts to mint tokens based on the difference between new voting power and user veRaac balance. Since voting power decays over time while balance remains constant, this calculation often results in a negative value, causing the function to revert and prevent users from increasing their locks.

Vulnerability Details

The veRAACToken contract implements a vote-escrowed token system where:

  • Users lock RAAC tokens to receive voting power

  • The voting power decays linearly over time based on the lock duration

  • The balance represents the initial user power and doesn't decay

The issue lies in the increase() function's minting logic:

function increase(uint256 amount) external nonReentrant {
// ... lock state updates ...
// Calculate new voting power
(int128 newBias, int128 newSlope) =
_votingState.calculateAndUpdatePower(msg.sender, userLock.amount + amount, userLock.end);
// Update checkpoints
uint256 newPower = uint256(uint128(newBias));
_checkpointState.writeCheckpoint(msg.sender, newPower);
// Transfer tokens
raacToken.safeTransferFrom(msg.sender, address(this), amount);
>> _mint(msg.sender, newPower - balanceOf(msg.sender));
}

The problem occurs because:

  1. balanceOf(msg.sender) represents the user initial power, which is constant

  2. newPower is calculated as bias = slope * timeLeft, which decreases over time

  3. As time passes, newPower becomes less than balanceOf(msg.sender)

  4. This makes newPower - balanceOf(msg.sender) negative often causing an overflow and function revert

For example:

  • User locks 1000 tokens for 1 year

  • Initially: newPower ≈ 1000, balance = 1000

  • After 6 months: newPower ≈ 500 (due to decay), balance = 1000

  • If user tries to increase by 100 tokens:

    • newPower ≈ 600

    • balance = 1000

    • mint amount = 600 - 1000 = -400 (reverts)

Impact

  • Users cannot increase their existing locks in case newPower < initialPower (balance),which breaks a core feature of the vote-escrowed token system

  • Affects governance participation as users can't increase their voting power

poc :

Foundry Envirement Setup
  • i'm using foundry for test , to integrate foundry :
    run :

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

    add this to hardhat.config.cjs :

    require("@nomicfoundation/hardhat-foundry");

    run :

    npx hardhat init-foundry
  • comment the test/unit/libraries/ReserveLibraryMock.sol as it's causing compiling errors

  • inside test folder , create new dir foundry and inside it , create new file baseTest.sol , and copy/paste this there :

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "forge-std/console2.sol";
import "../../contracts/core/tokens/DebtToken.sol";
import "../../contracts/core/tokens/RAACToken.sol";
import "../../contracts/core/tokens/veRAACToken.sol";
import "../../contracts/core/tokens/RToken.sol";
import "../../contracts/core/tokens/DEToken.sol";
import "../../contracts/core/pools/LendingPool/LendingPool.sol";
import "../../contracts/core/pools/StabilityPool/StabilityPool.sol";
import "../../contracts/core/tokens/RAACNFT.sol";
import "../../contracts/core/primitives/RAACHousePrices.sol";
import "../../contracts/mocks/core/tokens/crvUSDToken.sol";
import "../../contracts/core/collectors/FeeCollector.sol";
import "../../contracts/core/collectors/Treasury.sol";
import "../../contracts/core/minters/RAACMinter/RAACMinter.sol";
import "../../contracts/core/minters/RAACReleaseOrchestrator/RAACReleaseOrchestrator.sol";
contract baseTest is Test {
// Protocol contracts
crvUSDToken public crvUSD;
RAACToken public raacToken;
veRAACToken public veToken;
RAACHousePrices public housePrices;
RAACNFT public raacNFT;
RToken public rToken;
DebtToken public debtToken;
DEToken public deToken;
LendingPool public lendingPool;
StabilityPool public stabilityPool;
Treasury public treasury;
Treasury public repairFund;
FeeCollector public feeCollector;
RAACMinter public minter;
RAACReleaseOrchestrator public releaseOrchestrator;
// Test accounts
address public admin = makeAddr("admin");
address public user1 = makeAddr("user1");
address public user2 = makeAddr("user2");
address public user3 = makeAddr("user3");
// Constants
uint256 public constant INITIAL_MINT = 1000 ether;
uint256 public constant HOUSE_PRICE = 100 ether;
uint256 public constant INITIAL_PRIME_RATE = 0.1e27; // 10% in RAY
uint256 public constant TAX_RATE = 200; // 2% in basis points
uint256 public constant BURN_RATE = 50; // 0.5% in basis points
function setUp() public virtual {
vm.startPrank(admin);
// Deploy base tokens
crvUSD = new crvUSDToken(admin);
crvUSD.setMinter(admin);
raacToken = new RAACToken(admin, TAX_RATE, BURN_RATE);
veToken = new veRAACToken(address(raacToken));
releaseOrchestrator = new RAACReleaseOrchestrator(address(raacToken));
// Deploy mock oracle
housePrices = new RAACHousePrices(admin);
housePrices.setOracle(admin);
// Deploy NFT
raacNFT = new RAACNFT(address(crvUSD), address(housePrices), admin);
// Deploy pool tokens
rToken = new RToken("RToken", "RT", admin, address(crvUSD));
debtToken = new DebtToken("DebtToken", "DT", admin);
deToken = new DEToken("DEToken", "DEToken", admin, address(rToken));
// Deploy core components
treasury = new Treasury(admin);
repairFund = new Treasury(admin);
feeCollector =
new FeeCollector(address(raacToken), address(veToken), address(treasury), address(repairFund), admin);
lendingPool = new LendingPool(
address(crvUSD),
address(rToken),
address(debtToken),
address(raacNFT),
address(housePrices),
INITIAL_PRIME_RATE
);
stabilityPool = new StabilityPool(admin);
minter = new RAACMinter(address(raacToken), address(stabilityPool), address(lendingPool), address(treasury));
// Initialize contracts
raacToken.setFeeCollector(address(feeCollector));
raacToken.manageWhitelist(address(feeCollector), true);
raacToken.manageWhitelist(address(veToken), true);
raacToken.manageWhitelist(admin, true);
raacToken.setMinter(admin);
raacToken.mint(user2, INITIAL_MINT);
raacToken.mint(user3, INITIAL_MINT);
raacToken.setMinter(address(minter));
bytes32 FEE_MANAGER_ROLE = feeCollector.FEE_MANAGER_ROLE();
bytes32 EMERGENCY_ROLE = feeCollector.EMERGENCY_ROLE();
bytes32 DISTRIBUTOR_ROLE = feeCollector.DISTRIBUTOR_ROLE();
feeCollector.grantRole(FEE_MANAGER_ROLE, admin);
feeCollector.grantRole(EMERGENCY_ROLE, admin);
feeCollector.grantRole(DISTRIBUTOR_ROLE, admin);
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
raacToken.transferOwnership(address(minter));
rToken.transferOwnership(address(lendingPool));
debtToken.transferOwnership(address(lendingPool));
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(minter),
address(crvUSD),
address(lendingPool)
);
lendingPool.setStabilityPool(address(stabilityPool));
// Setup test environment
crvUSD.mint(user1, INITIAL_MINT);
crvUSD.mint(user2, INITIAL_MINT);
crvUSD.mint(user3, INITIAL_MINT);
housePrices.setHousePrice(1, HOUSE_PRICE);
vm.stopPrank();
}
// Helper functions
function mintCrvUSD(address to, uint256 amount) public {
vm.prank(admin);
crvUSD.mint(to, amount);
}
function setHousePrice(uint256 tokenId, uint256 price) public {
vm.prank(admin);
housePrices.setHousePrice(tokenId, price);
}
function calculateInterest(uint256 principal, uint256 rate, uint256 time) public pure returns (uint256) {
return principal * rate * time / 365 days / 1e27;
}
function warpAndAccrue(uint256 time) public {
vm.warp(block.timestamp + time);
lendingPool.updateState();
}
}
  • now create a pocs.sol inside test/foundry , and copy/paste this there :

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "./baseTest.sol";
import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockCurveVault is ERC4626 {
constructor(address _asset) ERC4626(ERC20(_asset)) ERC20("Mock Curve Vault", "mcrvUSD") {}
}
contract pocs is baseTest {
//poc here
}
  • here a test show the above scenario :

function test_poc10() public {
// Constants
uint256 INITIAL_LOCK = 1000 ether;
uint256 INCREASE_AMOUNT = 100 ether;
uint256 duration = 365 days;
// Step 1: User1 creates initial lock
vm.startPrank(address(minter));
raacToken.mint(user1, INITIAL_LOCK);
vm.stopPrank();
vm.startPrank(user1);
raacToken.approve(address(veToken), type(uint256).max);
veToken.lock(INITIAL_LOCK, duration);
// Record initial state
uint256 initialBalance = veToken.balanceOf(user1);
uint256 initialPower = veToken.getVotingPower(user1);
console.log("Initial balance :", initialBalance);
console.log("Initial voting power:", initialPower);
// Step 2: Wait 6 months - voting power should decay by ~50%
vm.warp(block.timestamp + 180 days);
// Step 3: Try to increase lock (should revert)
vm.startPrank(address(minter));
raacToken.mint(user1, INCREASE_AMOUNT);
vm.stopPrank();
vm.startPrank(user1);
// This will revert because:
// newPower (~600) - balance (1000) = -400
vm.expectRevert();
veToken.increase(INCREASE_AMOUNT);
// Log final state to show power decay
uint256 finalBalance = veToken.balanceOf(user1);
uint256 finalPower = veToken.getVotingPower(user1);
console.log("\nAfter 6 months:");
console.log("Balance (constant) :", finalBalance);
console.log("Voting power (decayed):", finalPower);
console.log("Power decay : ", (initialPower - finalPower) * 100 / initialPower, "%");
vm.stopPrank();
}

logs :

[PASS] test_poc10() (gas: 640737)
Logs:
Initial balance : 250000000000000000000
Initial voting power: 250000000000000000000
After 6 months:
Balance (constant) : 250000000000000000000
Voting power (decayed): 126712328767125568000
Power decay : 49 %

Tools Used

  • Foundry

  • Manual Review

Recommendations

The contract should properly handle both cases where the new voting power is higher or lower than the current balance:

function increase(uint256 amount) external nonReentrant {
// ... lock state updates ...
// Calculate new voting power
(int128 newBias, int128 newSlope) =
_votingState.calculateAndUpdatePower(msg.sender, userLock.amount + amount, userLock.end);
// Update checkpoints
uint256 newPower = uint256(uint128(newBias));
_checkpointState.writeCheckpoint(msg.sender, newPower);
// Transfer tokens
raacToken.safeTransferFrom(msg.sender, address(this), amount);
- _mint(msg.sender, newPower - balanceOf(msg.sender));
+ uint256 currentBalance = balanceOf(msg.sender);
+ if (newPower > currentBalance) {
+ _mint(msg.sender, newPower - currentBalance);
+ } else if (newPower < currentBalance) {
+ _burn(msg.sender, currentBalance - newPower);
+ }
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 3 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.