Summary
The _updateBoostState() function in veRAACToken.sol is invoked in the code logic of lock() and increase() before _votingState.calculateAndUpdatePower() and _mint(), leading to outdated calculations of totalSupply() and user's voting power. This results in an inaccurate boost calculation, affecting governance calculations dependent on the latest update of _boostState. To ensure correctness, _updateBoostState() should be called after _mint() to reflect the latest totalSupply() and updated voting power.
Vulnerability Details
In lock(), _updateBoostState() is called right after lock creation but before calculating initial voting power and minting veTokens:
veRAACToken.sol#L225-L241
_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);
At the point of triggering _updateBoostState(),
veRAACToken.sol#L568-L575
function _updateBoostState(address user, uint256 newAmount) internal {
_boostState.votingPower = _votingState.calculatePowerAtTimestamp(user, block.timestamp);
_boostState.totalVotingPower = totalSupply();
_boostState.totalWeight = _lockState.totalLocked;
_boostState.updateBoostPeriod();
}
_boostState.votingPower will assuredly be assigned 0 because point.timestamp is 0 since _votingState.calculateAndUpdatePower() has not be invoked yet to update state.points[user]:
VotingPowerLib.sol#L254-L271
RAACVoting.Point memory point = state.points[account];
if (point.timestamp == 0) return 0;
Additionally, the minted veTokens isn't reflected in totalSupply(). Hence, _boostState.totalVotingPower will be updated with a smaller value.
Similar issue is being seen in increase() with _boostState.votingPower assigned based on the existing userLock.amount instead of userLock.amount + amount. And apparently, _boostState.totalVotingPower will be updated with an outdated value.
Impact
All calculations protocol wide dependent on _boostState will be affected. As a paralleled example, BaseGauge._applyBoost() internally invoked by getUserWeight() which is being triggered by earned() is crucially needed when updating reward state for an account. As is evidenced in the code logic entailed, totalVotingPower: boostState.totalVotingPower and votingPower: boostState.votingPower are part of the params serving as the third input parameter for BoostCalculator.calculateBoost():
BaseGauge.sol#L236-L250
BoostCalculator.BoostParameters memory params = BoostCalculator.BoostParameters({
maxBoost: boostState.maxBoost,
minBoost: boostState.minBoost,
boostWindow: boostState.boostWindow,
totalWeight: boostState.totalWeight,
totalVotingPower: boostState.totalVotingPower,
votingPower: boostState.votingPower
});
uint256 boost = BoostCalculator.calculateBoost(
veBalance,
totalVeSupply,
params
);
Tools Used
Manual
Recommendations
Consider making the following fix:
veRAACToken.sol#L225-L241
// Create lock position
_lockState.createLock(msg.sender, amount, duration);
- _updateBoostState(msg.sender, amount);
// Calculate initial voting power
(int128 bias, int128 slope) = _votingState.calculateAndUpdatePower(
msg.sender,
amount,
unlockTime
);
// Update checkpoints
uint256 newPower = uint256(uint128(bias));
_checkpointState.writeCheckpoint(msg.sender, newPower);
// Mint veTokens
_mint(msg.sender, newPower);
+ _updateBoostState(msg.sender, amount);
veRAACToken.sol#L252-L270
// Increase lock using LockManager
_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));
+ _updateBoostState(msg.sender, locks[msg.sender].amount);