Summary
The lock function in the veRAACToken contract is responsible for creating a new lock position for users who lock their RAAC tokens to receive veRAAC tokens. However, the current implementation does not check whether a user already has an existing lock. This oversight allows users to call the lock function multiple times, overwriting their previous lock while still transferring additional raacToken to the contract. As a result, users may pay more raacToken than the amount actually locked, leading to financial loss and potential fairness issues in the protocol.
Vulnerability Details
The lock function calls createLock function to create lock position:
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);
...
...
In createLock function, it does not check whether the lock exists:
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;
}
When a user calls the lock function multiple times, the createLock function overwrites the previous lock record with the new one. However, the raacToken transfer still occurs for each call, meaning the user pays more raacToken than the amount reflected in their final lock.
POC
add the following test case into veRAACToken.test.js
it("the second lock will override the first one", async () => {
const amount = ethers.parseEther("1000");
const duration = 365 * 24 * 3600;
await veRAACToken.connect(users[0]).lock(amount, duration);
await veRAACToken.connect(users[0]).lock(amount, duration);
const balanceOfRaac = await raacToken.balanceOf(await veRAACToken.getAddress());
console.log("raac in veRAACToken:",balanceOfRaac);
const expected = ethers.parseEther("2000");
const position = await veRAACToken.getLockPosition(users[0].address);
expect(position.amount).to.equal(expected);
});
run npx hardhat test --grep "the second lock will override the first one"
veRAACToken
Lock Mechanism
raac in veRAACToken: 2000000000000000000000n
1) the second lock will override the first one
0 passing (15s)
1 failing
1) veRAACToken
Lock Mechanism
the second lock will override the first one:
AssertionError: expected 1000000000000000000000 to equal 2000000000000000000000.
+ expected - actual
-1000000000000000000000
+2000000000000000000000
We can see that 2000000000000000000000 raacToken has been sent to the contract but the lock amout is still 1000000000000000000000.
Impact
Users may lose funds as they pay more raacToken than the amount actually locked. The impact is High, the likelihood is Medium, so the severity is High.
Tools Used
Manual Review
Recommendations
To address this issue, the lock function should include a validation check to ensure that the user does not already have an existing lock. If a lock already exists, the function should revert with an appropriate error message.
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();
require(!_lockState.getLock(msg.sender).exists,"lock already exists");