Core Contracts

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

Calling _updateBoostState Before _mint in lock::veRAACToken.sol and increase::veRAACToken.sol

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();
// Do the transfer first - this will revert with ERC20InsufficientBalance if user doesn't have enough tokens
raacToken.safeTransferFrom(msg.sender, address(this), amount);
// Calculate unlock time
uint256 unlockTime = block.timestamp + duration;
// 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);
emit LockCreated(msg.sender, amount, unlockTime);
}
function increase(uint256 amount) external nonReentrant whenNotPaused {
// Increase lock using LockManager
_lockState.increaseLock(msg.sender, amount);
//@q should be called after _votingState.calculateAndUpdatePower
>> _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));
emit LockIncreased(msg.sender, amount);
}
function _updateBoostState(address user, uint256 newAmount) internal {
// Update boost calculator state
_boostState.votingPower = _votingState.calculatePowerAtTimestamp(user, block.timestamp); //@q user or global?
>> _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);
}
Updates

Lead Judging Commences

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

veRAACToken::_updateBoostState should be called later inside lock/increase

Support

FAQs

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

Give us feedback!