Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: medium
Valid

Missing Boost State Synchronization in `veRAACToken.extend` Causes Stale Voting Power Calculations

Summary

The veRAACToken.extend function fails to update boost controller state after modifying user voting power and total token supply, leading to stale boost calculations. This omission causes reward distributions and governance voting power to be calculated using outdated values until another state-changing operation occurs. The core issue stems from missing synchronization between lock duration extensions and boost parameter updates, violating the protocol's intended ve-token mechanics.

Vulnerability Details

The vulnerability exists in the lock extension mechanism of the veRAACToken contract where boost controller state updates are omitted after modifying voting power. When users extend their lock duration by calling veRAACToken.extend (veRAACToken.sol#L286-L302), the protocol:

  1. Updates voting power calculations through _votingState.calculateAndUpdatePower()

  2. Adjusts veToken balances via mint/burn operations

  3. Fails to update boost controller state with new voting power and total supply values

contract veRAACToken is ERC20, Ownable, ReentrancyGuard, IveRAACToken {
function extend(uint256 newDuration) external nonReentrant whenNotPaused {
// Extend lock using LockManager
uint256 newUnlockTime = _lockState.extendLock(msg.sender, newDuration);
// Update voting power
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
@> (int128 newBias, int128 newSlope) = _votingState.calculateAndUpdatePower(
msg.sender,
userLock.amount,
newUnlockTime
);
// Update checkpoints
uint256 oldPower = balanceOf(msg.sender);
uint256 newPower = uint256(uint128(newBias));
_checkpointState.writeCheckpoint(msg.sender, newPower);
// Update veToken balance
if (newPower > oldPower) {
@> _mint(msg.sender, newPower - oldPower);
} else if (newPower < oldPower) {
@> _burn(msg.sender, oldPower - newPower);
}
emit LockExtended(msg.sender, newUnlockTime);
}
}
library VotingPowerLib {
function calculateAndUpdatePower(
VotingPowerState storage state,
address user,
uint256 amount,
uint256 unlockTime
) internal returns (int128 bias, int128 slope) {
// ...
// Calculate initial voting power that will decay linearly to 0 at unlock time
@> uint256 duration = unlockTime - block.timestamp;
uint256 initialPower = (amount * duration) / MAX_LOCK_DURATION; // Normalize by max duration
@> bias = int128(int256(initialPower));
slope = int128(int256(initialPower / duration)); // Power per second decay
uint256 oldPower = getCurrentPower(state, user, block.timestamp);
@> state.points[user] = RAACVoting.Point({
@> bias: bias,
slope: slope,
timestamp: block.timestamp
});
// ...
}
function calculatePowerAtTimestamp(
VotingPowerState storage state,
address account,
uint256 timestamp
) internal view returns (uint256) {
RAACVoting.Point memory point = state.points[account];
if (point.timestamp == 0) return 0;
if (timestamp < point.timestamp) {
return 0;
}
uint256 timeDelta = timestamp - point.timestamp;
@> int128 adjustedBias = point.bias - (point.slope * int128(int256(timeDelta)));
return adjustedBias > 0 ? uint256(uint128(adjustedBias)) : 0;
}
}
contract veRAACToken is ERC20, Ownable, ReentrancyGuard, IveRAACToken {
function _updateBoostState(address user, uint256 newAmount) internal {
// Update boost calculator state
@> _boostState.votingPower = _votingState.calculatePowerAtTimestamp(user, block.timestamp);
@> _boostState.totalVotingPower = totalSupply();
_boostState.totalWeight = _lockState.totalLocked;
_boostState.updateBoostPeriod();
}
}

This omission leaves the boost controller's internal state (_boostState.votingPower and _boostState.totalVotingPower) stale, as these critical values are only updated in veRAACToken._updateBoostState() which is not called after balance changes.

The voting power calculations in VotingPowerLib rely on these boost state values being synchronized with actual token balances. When extended locks modify both user-specific voting power and total supply without updating the boost state, subsequent boost calculations will use outdated values leading to:

  • Incorrect user boost multipliers in gauge systems

  • Miscalculations of protocol-wide voting power ratios

  • Stale total supply values in time-weighted average computations

This creates systemic inaccuracies in reward distributions and governance power calculations until another state-modifying operation triggers an update.

Impact

This vulnerability has medium-severity systemic impacts:

  1. Reward Distribution Errors
    Gauge systems will calculate boosts using stale voting power data, leading to unfair reward allocations between users who recently modified locks vs inactive users.

  2. Governance Manipulation Risk
    Voting power discrepancies allow attackers to temporarily game proposal outcomes by timing lock extensions/withdrawals before boost state updates occur through other functions.

  3. Protocol Parameter Drift
    Time-weighted averages accumulate errors from incorrect total supply values, causing gradual miscalculations of system-wide boost ceilings and delegation limits.

While not directly enabling fund loss, this erodes core protocol guarantees about fair reward distribution and accurate governance power representation - critical trust factors for ve-token ecosystems.

Tools Used

Manual Review

Recommendations

Add boost state synchronization after voting power changes in veRAACToken.extend():

// Update veToken balance
if (newPower > oldPower) {
_mint(msg.sender, newPower - oldPower);
} else if (newPower < oldPower) {
_burn(msg.sender, oldPower - newPower);
}
+ _updateBoostState(msg.sender, userLock.amount);
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

veRAACToken::_updateBoostState not called in extend/withdraw

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.