Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: high
Valid

`veRAACToken::lock` Overwrites Existing Lock, Causing Loss of Funds for the User

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();
// Transfer tokens first - reverts if the user has insufficient balance
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);
}
function createLock(
LockState storage state,
address user,
uint256 amount,
uint256 duration
) internal returns (uint256 end) {
// Validate duration
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;
// @audit-issue Not checking if state.locks[user].exists == true before overwriting
@> 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; // 1 year
// Create first lock with 10 ETH
const tx1 = await veRAACToken.connect(users[0]).lock(amount1, duration);
await tx1.wait();
// Verify lock position
const position1 = await veRAACToken.getLockPosition(users[0].address);
expect(position1.amount).to.equal(amount1);
// Create second lock with 5 ETH
// This tx should fail or at least return a new position of 15 ETH, which does not
const amount2 = ethers.parseEther("5");
const tx2 = await veRAACToken.connect(users[0]).lock(amount2, duration);
await tx2.wait();
// Verify lock position
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.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

veRAACToken::lock called multiple times, by the same user, leads to loss of funds

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.