Summary
In both lock::veRAACToken.sol and increase::veRAACToken.sol, _updateBoostState is called before _mint. Since _mint increases totalSupply() by minting new veTokens (representing voting power), this ordering means _updateBoostState uses the pre-mint totalSupply() value rather than the updated value after the new veTokens are minted.
Vulnerability Details
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);
}
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);
}
function _updateBoostState(address user, uint256 newAmount) internal {
_boostState.votingPower = _votingState.calculatePowerAtTimestamp(user, block.timestamp);
>> _boostState.totalVotingPower = totalSupply();
>> _boostState.totalWeight = _lockState.totalLocked;
_boostState.updateBoostPeriod();
}
In lock, _mint(msg.sender, newPower) increases totalSupply() by newPower.
In increase, _mint(msg.sender, newPower - balanceOf(msg.sender)) increases totalSupply() by the difference between the new voting power and the user’s prior balance.
But _updateBoostState sets _boostState.totalVotingPower = totalSupply() before this increase occurs, also the totalSupply does not correspond to the _lockState.totalLocked, as _lockState.totalLocked would hav been increased and the totalSupply not increased accordingly
Impact
_boostState.totalVotingPower reflects the total veToken supply before the new voting power is minted, underrepresenting the system’s actual voting power at the end of the transaction.
_boostState.updateBoostPeriod() relies on totalVotingPower to calculate boosts it uses an outdated value, potentially skewing results for this transaction or subsequent ones.
Tools Used
Manual review
Recommendations
Move _updateBoostState after _mint in both functions to ensure it uses the updated totalSupply() and reflects the final state. Same in increase
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, locks[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); // Mint first
+ _updateBoostState(msg.sender, amount); // Then update boost state
emit LockCreated(msg.sender, amount, unlockTime);
}