Description
The veRAACToken::lock function incorrectly uses the raw lock amount parameter when checking against MAX_TOTAL_SUPPLY, rather than the actual veRAAC tokens to be minted. This prevents valid lock operations where the calculated voting power (based on lock duration) would stay under the supply cap but the raw amount exceeds remaining capacity.
Proof of Concept
Protocol has 99M veRAAC minted (MAX_TOTAL_SUPPLY = 100M)
User attempts to lock 1.5M RAAC for 2/3 of max duration (973 days)
Contract calculates voting power as (1.5M * 973d)/1460d = ~1M veRAAC (slightly less than 1M)
Current check totalSupply() + 1.5M = 100.5M fails
Valid lock attempt gets rejected despite having sufficient capacity
Test case to demonstrate vulnerability:
In veRAACToken.test.js, add this test and run npx hardhat test --grep "blocks valid locks due to incorrect supply check"
it("blocks valid locks due to incorrect supply check", async () => {
const lockAmountPerTx = ethers.parseEther("9900000");
const lockDuration = MAX_LOCK_DURATION;
for (let i = 0; i < 10; i++) {
await raacToken.mint(owner.address, lockAmountPerTx);
await raacToken
.connect(owner)
.approve(await veRAACToken.getAddress(), MaxUint256);
await veRAACToken.connect(owner).lock(lockAmountPerTx, lockDuration);
}
console.log("Owner lock completed");
const userLockAmount = ethers.parseEther("1500000");
const validDuration = 973 * 24 * 3600;
await raacToken.mint(users[0].address, userLockAmount);
await raacToken
.connect(users[0])
.approve(await veRAACToken.getAddress(), userLockAmount);
await expect(
veRAACToken.connect(users[0]).lock(userLockAmount, validDuration)
).to.be.revertedWithCustomError(veRAACToken, "TotalSupplyLimitExceeded");
});
Impact
High Severity - Permanently blocks legitimate users from creating locks when protocol approaches supply cap, directly violating core protocol functionality. Creates artificial limitation that shouldn't exist based on system design.
Recommendation
contracts/core/tokens/veRAACToken.sol
function lock(uint256 amount, uint256 duration) external {
// ... existing checks ...
- if (totalSupply() + amount > MAX_TOTAL_SUPPLY) revert TotalSupplyLimitExceeded();
+ uint256 veAmount = (amount * duration) / MAX_LOCK_DURATION;
+ if (totalSupply() + veAmount > MAX_TOTAL_SUPPLY) revert TotalSupplyLimitExceeded();
}
contracts/core/tokens/veRAACToken.sol
function lock(uint256 amount, uint256 duration) external {
- if (totalSupply() + amount > MAX_TOTAL_SUPPLY) revert TotalSupplyLimitExceeded();
// ... existing checks ...
// ... code after `newPower` is computed
+ if (totalSupply() + newPower > MAX_TOTAL_SUPPLY) revert TotalSupplyLimitExceeded();
}
function calculateVeAmount(uint256 amount, uint256 lockDuration) public view returns (uint256) {
return (amount * lockDuration) / MAX_LOCK_DURATION;
}
function _checkSupplyLimit(uint256 veAmount) internal view {
if (totalSupply() + veAmount > MAX_TOTAL_SUPPLY) {
revert TotalSupplyLimitExceeded();
}
}