Description
The veRAACToken contract allows users to lock RAAC tokens to receive voting power. When a user who already has a locked position calls veRAACToken::lock
function again, their previous position is overwritten without returning their tokens, leading to permanent loss of funds.
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);
}
Furthermore LockManager::createLock
function called by veRAACToken::lock
has no check for existing lock position:
function createLock(
LockState storage state,
address user,
uint256 amount,
uint256 duration
) internal returns (uint256 end) {
if (state.minLockDuration != 0 && state.maxLockDuration != 0) {
if (duration < state.minLockDuration || duration > state.maxLockDuration)
revert InvalidLockDuration();
}
if (amount == 0) revert InvalidLockAmount();
end = block.timestamp + duration;
@>
@> state.locks[user] = Lock({
@> amount: amount,
@> end: end,
@> exists: true
@> });
@> state.totalLocked += amount;
emit LockCreated(user, amount, end);
return end;
}
Risk
Likelihood: Medium
Impact: High
Recommended Mitigation
The protocol should add a separate function for increasing existing locks and preventing overwriting existing locks in veRAACToken::lock
function