Core Contracts

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

The `veRAACToken::lock()` function unintentionally overwrites the existing lock data in `_lockState.locks[user]`, potentially causing users to lose all previously locked tokens.

Summary

The veRAACToken::lock() function unintentionally overwrites the existing lock data in _lockState.locks[user], potentially causing users to lose all previously locked tokens.

Vulnerability Details

The lock() function is designed to allow users to lock tokens. However, as indicated by the @> mark, the createLock() function in _lockState directly updates _lockState.locks[user]. Since the function does not check for an existing lock before writing new data, calling lock() again will overwrite the user's previous lock, effectively erasing all previously locked tokens.

// veRAACToken::lock()
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();
// Do the transfer first - this will revert with ERC20InsufficientBalance if user doesn't have enough tokens
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);
}

Poc

The following test demonstrates the issue by creating a lock and then calling lock() again with a different amount, which overwrites the original lock data.

Add the following test to test/unit/core/tokens/veRAACToken.test.js and execute it:

describe("Lock accidental overwriting of data", () => {
it("Poc", async () => {
// Cache initial balance of users[0]
const userBalanceStart = await raacToken.balanceOf(users[0].address);
const amount = ethers.parseEther("1000");
const duration = 365 * 24 * 3600 * 4 ; // 4 year
// Create lock first
const tx = await veRAACToken.connect(users[0]).lock(amount, duration);
// Wait for transaction confirmation
const receipt = await tx.wait();
// Find the LockCreated event
const event = receipt.logs.find(
log => log.fragment && log.fragment.name === 'LockCreated'
);
// Get the actual unlock time from the event
const actualUnlockTime = event.args[2];
// Verify lock position
const position = await veRAACToken.getLockPosition(users[0].address);
expect(position.amount).to.equal(amount);
expect(position.end).to.equal(actualUnlockTime);
expect(position.power).to.be.equal(amount);
// Call lock() again with a different amount
await veRAACToken.connect(users[0]).lock(ethers.parseEther("1"), duration);
// Verify that the lock position has been overwritten
const checkPosition = await veRAACToken.getLockPosition(users[0].address);
expect(checkPosition.amount).to.equal(ethers.parseEther("1")); // Original amount lost
// Simulate time passing to allow withdrawal
await time.increase(365 * 24 * 3600 * 4 + 100);
await network.provider.send("evm_mine");
// Withdraw funds
await veRAACToken.connect(users[0]).withdraw();
const userBalanceEnd = await raacToken.balanceOf(users[0].address);
console.log("users[0] loss amount:",userBalanceStart - userBalanceEnd);
});
});

output:

veRAACToken
Lock accidental overwriting of data
users[0] loss amount: 1000000000000000000000n

Impact

A user who calls lock() more than once will unintentionally overwrite their existing lock, causing them to lose all previously locked tokens.

Tools Used

Manual Review

Recommendations

To prevent accidental data overwriting, add a check to _lockState.locks[msg.sender].amount and revert if a lock already exists:, for example:

+ error lockDataAlreadyExists();
function lock(uint256 amount, uint256 duration) external nonReentrant whenNotPaused {
if (amount == 0) revert InvalidAmount();
if (amount > MAX_LOCK_AMOUNT) revert AmountExceedsLimit();
+ if (_lockState.locks[msg.sender].amount > 0) revert lockDataAlreadyExists();
// SNIP...
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months 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.

Give us feedback!