Description
When lock() is called by a user, it internally calls createLock()
:
File: contracts/core/tokens/veRAACToken.sol
212: function lock(uint256 amount, uint256 duration) external nonReentrant whenNotPaused {
213: if (amount == 0) revert InvalidAmount();
214: if (amount > MAX_LOCK_AMOUNT) revert AmountExceedsLimit();
215: if (totalSupply() + amount > MAX_TOTAL_SUPPLY) revert TotalSupplyLimitExceeded();
216: if (duration < MIN_LOCK_DURATION || duration > MAX_LOCK_DURATION)
217: revert InvalidLockDuration();
218:
219:
220: raacToken.safeTransferFrom(msg.sender, address(this), amount);
221:
222:
223: uint256 unlockTime = block.timestamp + duration;
224:
225:
226:@---> _lockState.createLock(msg.sender, amount, duration);
227: _updateBoostState(msg.sender, amount);
228:
229:
230: (int128 bias, int128 slope) = _votingState.calculateAndUpdatePower(
231: msg.sender,
232: amount,
233: unlockTime
234: );
235:
236:
237: uint256 newPower = uint256(uint128(bias));
238: _checkpointState.writeCheckpoint(msg.sender, newPower);
239:
240:
241: _mint(msg.sender, newPower);
242:
243: emit LockCreated(msg.sender, amount, unlockTime);
244: }
If a user calls lock()
again, this will overwrite the previous _lockState
since there is no check if a lock already exists for the user.
Impact
User permanently loses previously locked tokens and can't withdraw them.
Proof of Concept
Add this inside the describe("Lock Mechanism"
section of test/unit/core/tokens/veRAACToken.test.js
and run to see it pass:
it("should not allow creating multiple locks for the same user", async function () {
const amount = ethers.parseEther("1000");
const duration = 365 * 24 * 3600;
await veRAACToken.connect(users[1]).lock(amount, duration);
await veRAACToken.connect(users[1]).lock(amount, duration);
const position = await veRAACToken.getLockPosition(users[1].address);
expect(position.amount).to.equal(amount);
});
Mitigation
Add the following check in lock()
:
require(!_lockState.locks[msg.sender].exists, "lock already exists");