Summary
The veRAACToken contract does not verify if a user already has an active lock before allowing them to create a new lock. This oversight could result in multiple locks being created for a single user, and more critically, users will not be able to withdraw previously locked tokens, potentially leading to permanent loss of their tokens.
Vulnerability Details
In the lock()
function, there is no check to see if a user already has an active lock position. Without this check, a user can accidentally or intentionally create multiple locks, which may prevent them from being able to withdraw their previously locked tokens. If users cannot properly withdraw their tokens because their state is not properly managed, they may face permanent token loss.
Relevant Code:
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);
}
Impact
Multiple Locks Per User: Without a check to ensure a user has only one active lock, users may create multiple locks. This could lead to complications in withdrawing their tokens as the contract might not be able to manage multiple locks effectively.
Permanent Token Loss: Users could suffer permanent loss of their tokens if they are unable to withdraw locked tokens due to conflicting lock states. The contract might not allow withdrawing tokens locked under multiple positions, leading to unintended and irreversible loss.
Voting Power and Boost Calculation: Users may incorrectly accumulate voting power or boosts based on multiple locks, leading to inaccurate rewards or governance influence.
Security Risk: The absence of validation could be exploited by users to bypass intended limitations on the contract, resulting in potential operational errors and confusion.
Tools Used
Manual code inspection
Recommendations
Ensure that the contract checks if a user already has an active lock before allowing a new one.
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();
+ // Check if the user already has an active lock
+ LockManager.Lock memory userLock = _lockState.locks[msg.sender];
+ if (userLock.exists) revert ExistingLockDetected(); // Prevent creating a new lock if one already exists
// 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);
}