Summary
The veRAACToken
contract incorrectly updates _boostState.totalVotingPower
before minting tokens, leading to a discrepancy where the total voting power is one step behind the actual minted supply. This results in incorrect boost calculations, affecting user rewards and governance voting power.
Vulnerability Details
The lock
as well as increase
functions in veRAACToken
updates _boostState.totalVotingPower
using totalSupply()
before minting the corresponding veRAAC tokens. As a result, the voting power calculations do not account for the newly minted tokens in the current transaction, causing a underestimation of total voting power.
Here is the code of 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();
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);
}
Impact
Any calculation that relies on _boostState.totalVotingPower
or veRAACToken.getBoostState
will be incorrect.
Proof of Concept
Here is the test case that shows the issue:
it.only("H04 - Inconsistent totalVotingPower Calculation in veRAACToken", async () => {
const alice = users[0];
const bob = users[1];
const amount = ethers.parseEther("1000");
const amount2 = ethers.parseEther("1500");
const duration = 365 * 24 * 3600 * 4;
console.log("Creating first lock with amount 1000...");
const alice1Tx = await veRAACToken.connect(alice).lock(amount, duration);
await alice1Tx.wait();
const boostState = await veRAACToken.getBoostState();
console.log("Total Voting Power after first lock: ", boostState[3]);
console.log("totalWeight after first lock: ", boostState[4]);
console.log("Creating second lock with amount 1500...");
const bob1Tx = await veRAACToken.connect(bob).lock(amount2, duration);
await bob1Tx.wait();
const boostState2 = await veRAACToken.getBoostState();
console.log("Total Voting Power after second lock: ", boostState2[3]);
console.log("totalWeight after second lock: ", boostState2[4]);
});
and the output of the test is:
Creating first lock with amount 1000...
Total Voting Power after first lock: 0n
totalWeight after first lock: 1000000000000000000000n
Creating second lock with amount 1500...
Total Voting Power after second lock: 1000000000000000000000n
totalWeight after second lock: 2500000000000000000000n
Tools Used
Manual Review
Recommendations
Ensure veRAACToken::updateBoostState()
is called after minting tokens.