Summary
Contract - veRAAC.sol
The _updateBoostState() function is as follow -
function _updateBoostState(address user, uint256 newAmount) internal {
@-> _boostState.votingPower = _votingState.calculatePowerAtTimestamp(user, block.timestamp);
_boostState.totalVotingPower = totalSupply();
_boostState.totalWeight = _lockState.totalLocked;
_boostState.updateBoostPeriod();
}
By defination votingPower of a boost state, represents the voting power of all user (in boost period).
But in current implementation, the voting power only accounts for 1 user, which is wrong.
Vulnerability Details
wrong updation of _boostState.votingPower can be very drastic.
_boostState.votingPower is designed to store collecting voting power of all user, in the particular boost period.
Let's go through function flow (only relevant ones) -
lock(uint256 amount, uint256 duration) ->
_updateBoostState(msg.sender, amount) ->
updateBoostPeriod(BoostState storage state) ->
updateValue(Period storage self, uint256 newValue, uint256 timestamp)
verRAAC.sol::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);
}
veRAAC.sol::_updateBoostState()
function _updateBoostState(address user, uint256 newAmount) internal {
@-> _boostState.votingPower = _votingState.calculatePowerAtTimestamp(user, block.timestamp);
_boostState.totalVotingPower = totalSupply();
_boostState.totalWeight = _lockState.totalLocked;
@-> _boostState.updateBoostPeriod();
}
BoostCalculator.sol::updateBoostPeriod()
function updateBoostPeriod(
BoostState storage state
) internal {
if (state.boostWindow == 0) revert InvalidBoostWindow();
if (state.maxBoost < state.minBoost) revert InvalidBoostBounds();
uint256 currentTime = block.timestamp;
uint256 periodStart = state.boostPeriod.startTime;
if(periodStart > 0) {
if (currentTime >= periodStart + state.boostWindow) {
TimeWeightedAverage.createPeriod(
state.boostPeriod,
currentTime,
state.boostWindow,
state.votingPower,
state.maxBoost
);
return;
}
@-> state.boostPeriod.updateValue(state.votingPower, currentTime);
return;
}
TimeWeightedAverage.createPeriod(
state.boostPeriod,
currentTime,
state.boostWindow,
state.votingPower,
state.maxBoost
);
}
TimeWeightedAverage.sol::updateValue()
function updateValue(
Period storage self,
uint256 newValue,
uint256 timestamp
) internal {
if (timestamp < self.startTime || timestamp > self.endTime) {
revert InvalidTime();
}
unchecked {
uint256 duration = timestamp - self.lastUpdateTime;
if (duration > 0) {
uint256 timeWeightedValue = self.value * duration;
if (timeWeightedValue / duration != self.value) revert ValueOverflow();
self.weightedSum += timeWeightedValue;
self.totalDuration += duration;
}
}
@-> self.value = newValue;
self.lastUpdateTime = timestamp;
}
Impact
-
The problem here is that self.value is being updated with voting power of a user, which is wrong. state.value should
account for all users during that boost duration.
-
Wheneven a new user creates a new lock, self.value will again be reupdated to only account for new user.
-
Boost state value or self.value is being used to calculate user Reward, it's incorrect value means incorrect reward to user.
Tools Used
Manual
Recommendations
Instead of updating self.value for 1 user only, built as functionality that account for all user in that boost period.