Core Contracts

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

Limited veRaac Token Supply Triggers DoS, Hampering Proper Governance Participation.

Summary

In the veRAACToken contract, there is an external, non-reentrant, and pausable function called lock. This function locks raacTokens and issues a proportional amount of veRaacTokens. It enforces several restrictions: the locking amount must not be zero, it must not exceed the MAX_LOCK_AMOUNT constant (10M tokens with 18 decimal precision), the lock duration must fall within the allowed range (MIN_LOCK_DURATION ≤ duration ≤ MAX_LOCK_DURATION), and the total supply of veRaacTokens must not exceed the MAX_TOTAL_SUPPLY constant (100M tokens with 18 decimal precision).

However, there is a flaw in the total supply restriction check. The totalSupply() function returns the total number of veRaacTokens in circulation, while the amount parameter represents the quantity of raacTokens that are about to be locked. Because these values refer to different tokens—totalSupply() for veRaacTokens and amount for raacTokens—this discrepancy can lead to a Denial of Service (DoS) scenario. As the total supply of veRaacTokens nears its maximum, the protocol may be unable to issue the full 100M veRaacTokens. This, in turn, can adversely affect the quorum in the Governance contract and stifle potential competition by preventing further issuance of veRaacTokens.

Vulnerability Details

veRAACToken::lock:

function lock(uint256 amount, uint256 duration) external nonReentrant whenNotPaused {
if (amount == 0) revert InvalidAmount();
if (amount > MAX_LOCK_AMOUNT) revert AmountExceedsLimit();
@> if (totalSupply() + amount > MAX_TOTAL_SUPPLY) revert TotalSupplyLimitExceeded();
if (duration < MIN_LOCK_DURATION || duration > MAX_LOCK_DURATION) {
revert InvalidLockDuration();
}
// Do the transfer first - this will revert with ERC20InsufficientBalance if user doesn't have enough tokens
raacToken.safeTransferFrom(msg.sender, address(this), amount);
// Calculate unlock time
uint256 unlockTime = block.timestamp + duration;
// Create lock position
_lockState.createLock(msg.sender, amount, duration);
_updateBoostState(msg.sender, amount);
// Calculate initial voting power
(int128 bias, int128 slope) = _votingState.calculateAndUpdatePower(msg.sender, amount, unlockTime);
// Update checkpoints
uint256 newPower = uint256(uint128(bias));
_checkpointState.writeCheckpoint(msg.sender, newPower);
// Mint veTokens
_mint(msg.sender, newPower);
emit LockCreated(msg.sender, amount, unlockTime);
}

Governance::quorum:

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

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 testLockAmountVeRaacTokenTotalSupplyWrongComparison() 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();
}
console.log("total supply max: ", veRaacToken.MAX_TOTAL_SUPPLY());
console.log("total supply : ", veRaacToken.getTotalVotingPower());
// now there's 90M total supply of veRAAC tokens
// and 90 + 10 = 100M
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();
console.log("after adding last 10M");
console.log("total supply max: ", veRaacToken.MAX_TOTAL_SUPPLY());
console.log("total supply : ", veRaacToken.getTotalVotingPower());
// now there's 92.5M total supply of veRaac tokens
// and now can't lock next 10M raac tokens due to the comparison flaw
vm.startPrank(RAAC_MINTER);
raacToken.mint(BOB, LOCK_AMOUNT);
vm.stopPrank();
vm.startPrank(BOB);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
vm.expectRevert(bytes4(keccak256("TotalSupplyLimitExceeded()")));
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
// we can add only 100M - 92.5M raacTokens
uint256 canLockAmount = veRaacToken.MAX_TOTAL_SUPPLY() - veRaacToken.getTotalVotingPower();
vm.startPrank(RAAC_MINTER);
raacToken.mint(BOB, canLockAmount);
vm.stopPrank();
vm.startPrank(BOB);
raacToken.approve(address(veRaacToken), canLockAmount);
veRaacToken.lock(canLockAmount, LOCK_DURATION);
vm.stopPrank();
console.log("after adding last 7.5M");
console.log("total supply max: ", veRaacToken.MAX_TOTAL_SUPPLY());
console.log("total supply : ", veRaacToken.getTotalVotingPower());
// now there's 94.375M total supply of veRaac Tokens available
// again can't lock full 10M raac Tokens
// we can lock only 100M - 94.375M = 5.625M raac Tokens
// notice this diminishing rate of lockable raac token
// it's all happening because of that invalid wrong comparison
// total supply can't reach to its full potential
// users can't lock their full potential raac tokens of 10M
// what was expected?
// 1e18 raacToken = 0.25e18 veRaacToken for 1 year
// 10e18 raacToken = 2.5e18 veRaacToken for 1 year
// 100e18 raactoken = 250e18 veRaacToken for 1 year
// ...
// raacToken * duration / max_duration = veRaacToken
// raacToken * duration = veRaacToken * max_duration
// raacToken = veRaacToken * max_duration / duration
// raacToken = 100M * 4years / 1 year
// raacToken = 400M == 100M veRaacToken
// and raacToken = 100M for 4 years
}
  1. Step 6: To run the test, execute the following commands in your terminal:

forge test --mt testLockAmountVeRaacTokenTotalSupplyWrongComparison -vv
  1. Step 7: Review the output. The expected output should indicate that there's a Potential DoS due to the diminishing rate of the lockable raac tokens.

[⠒] Compiling...
No files changed, compilation skipped
Ran 1 test for test/VeRAACTokenTest.t.sol:VeRAACTokenTest
[PASS] testLockAmountVeRaacTokenTotalSupplyWrongComparison() (gas: 8887711)
Logs:
total supply max: 100000000000000000000000000
total supply : 90000000000000000000000000
after adding last 10M
total supply max: 100000000000000000000000000
total supply : 92500000000000000000000000
after adding last 7.5M
total supply max: 100000000000000000000000000
total supply : 94375000000000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 134.26ms (37.78ms CPU time)
Ran 1 test suite in 366.10ms (134.26ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

As demonstrated, the test confirms that a Denial of Service (DoS) is possible overtime.

Impact

  • Reduces the quorum in governance.

  • Stifles potential competition by preventing further issuance of veRaacTokens.

  • Leads to a gradual Denial of Service (DoS) over time due to the diminishing rate of lockable raacTokens.

  • Undermines the protocol's intended functionality.

Tools Used

  • Manual Review

  • Foundry

  • Console Log (foundry)

Recommendations

The raacToken amount to be locked is first converted into the equivalent veRaacToken amount, which is then added to the current totalSupply(). This sum is subsequently compared with the MAX_TOTAL_SUPPLY.

One possible solution is as follows:

function lock(uint256 amount, uint256 duration) external nonReentrant whenNotPaused {
if (amount == 0) revert InvalidAmount();
if (amount > MAX_LOCK_AMOUNT) revert AmountExceedsLimit();
- if (totalSupply() + amount > MAX_TOTAL_SUPPLY) revert TotalSupplyLimitExceeded();
if (duration < MIN_LOCK_DURATION || duration > MAX_LOCK_DURATION) {
revert InvalidLockDuration();
}
+ if (totalSupply() + calculateVeAmount(amount, duration) > MAX_TOTAL_SUPPLY) revert TotalSupplyLimitExceeded();
// Do the transfer first - this will revert with ERC20InsufficientBalance if user doesn't have enough tokens
raacToken.safeTransferFrom(msg.sender, address(this), amount);
// Calculate unlock time
uint256 unlockTime = block.timestamp + duration;
// Create lock position
_lockState.createLock(msg.sender, amount, duration);
_updateBoostState(msg.sender, amount);
// Calculate initial voting power
(int128 bias, int128 slope) = _votingState.calculateAndUpdatePower(msg.sender, amount, unlockTime);
// Update checkpoints
uint256 newPower = uint256(uint128(bias));
_checkpointState.writeCheckpoint(msg.sender, newPower);
// Mint veTokens
_mint(msg.sender, newPower);
emit LockCreated(msg.sender, amount, unlockTime);
}
Updates

Lead Judging Commences

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

Incorrect `MAX_TOTAL_SUPPLY` check in the `veRAACToken::lock/extend` function of `veRAACToken` could harm locking functionality

Support

FAQs

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