Summary
The veRAACToken::lock
function allows a user to create a lock by specifying an amount of RAAC tokens and a duration. Internally, the function calls LockManager::createLock
, which overwrites any existing lock for the caller. As a result, if a user calls the lock
function twice, the tokens locked in the first call will be lost, and the lock duration/end date will be overwritten.
Vulnerability Details
The function createLock
directly assigns a new Lock
struct to state.locks[user]
, replacing any existing lock without checking whether a lock already exists for the given user
.
When a user attempts to create a second lock, the previous lock’s amount is lost.
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);
}
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;
}
PoC
Add the following test to test/unit/core/tokens/veRAACToken.test.js
.
it.only("should revert if attempting to lock twice", async () => {
const amount1 = ethers.parseEther("10");
const duration = 365 * 24 * 3600;
const tx1 = await veRAACToken.connect(users[0]).lock(amount1, duration);
await tx1.wait();
const position1 = await veRAACToken.getLockPosition(users[0].address);
expect(position1.amount).to.equal(amount1);
const amount2 = ethers.parseEther("5");
const tx2 = await veRAACToken.connect(users[0]).lock(amount2, duration);
await tx2.wait();
const position2 = await veRAACToken.getLockPosition(users[0].address);
expect(position2.amount).to.equal(amount2);
});
Impact
Users who attempt to lock additional tokens lose access to their previously locked funds.
Tools Used
Manual Review
Recommendations
Check if a lock already exists before creating a new one.