Core Contracts

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

The `veRAACToken.balanceOf(user)` which is same as power of user does not decay over time and this leads to several errors.

Summary

The veRAACToken.balanceOf(user) which is same as power of user does not decay over time and this leads to several errors.

Vulnerability Details

We can see that voting power decays over time from following doc.

/**
* @title Vote Escrowed RAAC Token
* @author RAAC Protocol Team
* @notice A vote-escrowed token contract that allows users to lock RAAC tokens to receive voting power and boost capabilities
* @dev Implementation of vote-escrowed RAAC (veRAAC) with time-weighted voting power, emergency controls, governance integration and boost calculations
* Key features:
* - Users can lock RAAC tokens for voting power
@> * - Voting power decays linearly over time
* - Includes emergency withdrawal mechanisms
* - Integrates with governance for proposal voting
* - Provides boost calculations for rewards
*/
contract veRAACToken is ERC20, Ownable, ReentrancyGuard, IveRAACToken {
...
}

And we can see that veRAACToken.balanceOf(user) is same as power of user.

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);
_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 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);
}
function withdraw() external nonReentrant {
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
if (userLock.amount == 0) revert LockNotFound();
if (block.timestamp < userLock.end) revert LockNotExpired();
uint256 amount = userLock.amount;
@> uint256 currentPower = balanceOf(msg.sender);
// Clear lock data
delete _lockState.locks[msg.sender];
delete _votingState.points[msg.sender];
// Update checkpoints
_checkpointState.writeCheckpoint(msg.sender, 0);
// Burn veTokens and transfer RAAC
_burn(msg.sender, currentPower);
raacToken.safeTransfer(msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}

As we can see above, balanceOf(user) seems to be same as power.
The veRAACToken derived ERC20 and does not override balanceOf() function.
So veRAACToken's balanceOf() is simple ERC20 balanceOf() function, not rebasing over time.

Impact

This leads to broken veRAACToken logic and voting power.
For example, legitimate call of increase() function can be reverted.
PoC:

  • A user locks 1e18 of raacToken for 365 days.

  • Then, he receives veRAACToken: 1e18 * 365 days / 1460 days = 0.25e18.

uint256 initialPower = (amount * duration) / MAX_LOCK_DURATION; // Normalize by max duration
  • The time elapsed half year - 365 days / 2.

  • Then, he increase 0.5e18 of raacToken.

function increase(uint256 amount) external nonReentrant whenNotPaused {
// 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);
270 _mint(msg.sender, newPower - balanceOf(msg.sender));
emit LockIncreased(msg.sender, amount);
}

Here, userLock.amount = 1e18, amount = 0.5e18.
Then, newBias = 1.5e18 * 365 days / 2 / 1460 days = 1.5e18 / 8 = 0.1875e18.
But balanceOf(msg.sender) = 0.25e18 > newBias(newPower).
So L270 is reverted.

On the other hand, GaugeController.sol#vote() function uses veRAACToken.balaceOf(user) as voting power.

This is big error.

Tools Used

Manual review

Recommendations

  • Please override veRAACToken.balanceOf() to decay over time as voting power.

  • Please override veRAACToken.getTotalVotingPower to decay over time as voting power so that it is same as sum of users' voting power.

Updates

Lead Judging Commences

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

veRAACToken::increase underflows on newPower - balanceOf(msg.sender)

Support

FAQs

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

Give us feedback!