Summary
The veRAACToken::lock function does not checks if the user already holds a lock or not, hence if a user creates a lock twice, the old position is lost along with the funds.
Vulnerability Details
The veRAACToken::lock function is designed to allow users to lock their RAACTokens in order to obtain voting rights which is represented as veRAACTokens.
function lock(uint256 amount, uint256 duration) external nonReentrant whenNotPaused {
raacToken.safeTransferFrom(msg.sender, address(this), amount); <<@ --
uint256 unlockTime = block.timestamp + duration;
_lockState.createLock(msg.sender, amount, duration);
_updateBoostState(msg.sender, amount);
_mint(msg.sender, newPower); <<@ --
emit LockCreated(msg.sender, amount, unlockTime);
}
However, upon observation we can determine that this function does not check if the user holds any current position in the _lockState.
Hence, if a user decides to create a new lock with either intention of creating a different lock period or simply increasing their voting power or simply a mistake, the old position would be over-written leading to loss of funds as the withdraw veRAACToken::withdraw function uses the lock state's locked amount.
function withdraw() external nonReentrant {
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
if (userLock.amount == 0) revert LockNotFound();
if (block.timestamp < userLock.end) revert LockNotExpired();
uint256 amount = userLock.amount; <<@ -
Impact
Overwrittes state with new duration.
Loss of funds as the withdraw function leverages the over-written state for withdrawal.
The view function veRAACToken::_updateBoostState is incorrect as the boostState.totalWeight will now contain incorrect value which is updated when tokens are locked, but as we withdraw the old veRAAC tokens are not actually locked here but burnt and the accounting can be deemed lost in oblivion.
Proof of Concept
Add the following test case inside the veRAACToken.test.js file:
describe("Improper Lock Mechanism", () => {
it("should allow creating a lock twice", async () => {
const amount = ethers.parseEther("100");
const duration = 365 * 24 * 3600;
const tx = await veRAACToken.connect(users[0]).lock(amount, duration);
const receipt = await tx.wait();
const event = receipt.logs.find(
log => log.fragment && log.fragment.name === 'LockCreated'
);
const position = await veRAACToken.getLockPosition(users[0].address);
console.log("Position 1: ", position);
const tx1 = await veRAACToken.connect(users[0]).lock(ethers.parseEther('10'), duration);
const receipt1 = await tx1.wait();
const event1 = receipt1.logs.find(
log => log.fragment && log.fragment.name === 'LockCreated'
);
const position1 = await veRAACToken.getLockPosition(users[0].address);
console.log("Position 2: ", position1);
});
});
Tools Used
Manual Review
Hardhat
Recommendations
It is recommended to check if there's a lock present:
function lock(uint256 amount, uint256 duration) external nonReentrant whenNotPaused {
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
if (userLock.amount > 0) revert LockFound();
}