Summary
The veRAACToken::lock
function does not prevent a user from creating a new lock when an existing lock already exists. As a result, if a user calls lock more than once, the initial lock is overwritten and its benefits (such as voting power or boost) are lost, leading to potential value loss for the user.
Vulnerability Details
The lock
function in the veRAACToken
contract is designed to transfer RAAC tokens from the user, create and store a new lock, and mint corresponding veRAAC tokens representing voting power. However, the current implementation does not check whether a user already has an existing lock. This means that if a user calls lock a second time:
The previous lock state is overwritten by the new lock.
The voting power and boost calculations of the previous lock are lost.
The user's previously locked tokens become effectively "lost" or non-recoverable under the intended benefit scheme, as only the new lock will be active.
The relevant snippet is:
unction 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);
}
There is no check to verify whether a lock already exists for a user. Thus, subsequent calls to lock by the same user will override or replace the prior lock without any safeguards—resulting in a loss of the previous lock’s values and benefits.
Proof of Concept
Append the follow code to the veRaacToken.test.js
file
it("calling lock again lead to direct lose of values for user", async () => {
const initalBalanceOfUser = await raacToken.balanceOf(users[0].address);
const amount = ethers.parseEther("500");
const duration = 365 * 24 * 3600;
await veRAACToken.connect(users[0]).lock(amount, duration);
const position = await veRAACToken.getLockPosition(users[0].address);
expect(position.amount).to.equal(amount);
expect(position.power).to.be.gt(0);
await veRAACToken.connect(users[0]).lock(amount, duration);
const position2 = await veRAACToken.getLockPosition(users[0].address);
expect(position2.amount).to.equal(amount);
expect(position2.power).to.be.gt(0);
const balanceAfter = await raacToken.balanceOf(users[0].address);
expect(initalBalanceOfUser - balanceAfter).to.equal(amount * ethers.toBigInt("2"));
});
Impact
Loss of Locked Benefits: Users who inadvertently or maliciously call lock multiple times lose the benefits (voting power and boost) from their initial lock.
Economic Disadvantage: Overwriting an existing lock could lead to underrepresentation in governance and reduced reward boosts.
User Confusion: The absence of a safeguard might lead users to believe that multiple locks can coexist, when in fact any additional call resets their lock state, causing potential loss of funds or misallocation of voting power.
Tools Used
Recommendations
Prevent users from calling lock
if they already possess an active lock. A simple mitigation is to add a check at the beginning of the function to revert if a lock already exists. For example, modify the lock function as follows:
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();
}
+ // Prevent creating a new lock if one already exists for the user.
+ if (_lockState.getLock(msg.sender).exists) {
+ revert LockAlreadyExists();
+ }
// 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);
}