Core Contracts

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

`veRaac::lock` doesn't check for existing lock. Users may loose a part of their locked Raac tokens

Summary

`veRaacToken::lock` function transfers Raac tokens from ms.sender and creates the lock without checking if msg.sender has an existing lock.
Users may loose tokens transferred in previous lock calls.

Vulnerability Details

Users can call lock function to create a new lock position and mint veRaac tokens.
First, the amount and duration are checked to not surpass the maximum values configured.
Then the amount of Raac token is transferred from msg.sender, the lockState.createLock is called to create the actual lock position and, in the end, the newPower amount of veRaac is minted.

// veRaacToken.sol
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);
}
//LockManager.sol
function createLock(
LockState storage state,
address user,
uint256 amount,
uint256 duration
) internal returns (uint256 end) {
// Validation logic remains the same
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;
}

The lock function doesn't check if the caller has an existing lock position.
An user can call lock twice with the intention to create two different positions with different arguments - amount and lock duration.
However, the first lock position will be overwritten by the last position created.As a result, after the period expires, the user will be able to withdraw only the last locked amount.

Add the following test under Lock Mechanism test suite from

test/unit/core/tokens/veRAACToken.test.js file.

Run the test with npx hardhat test --grep "should allow users to increase lock amount" command.

it("should increase the user's locked amount when locking more than once", async () => {
const amount = ethers.parseEther("1000");
const halfAmount = ethers.parseEther("500");
const totalAmount = ethers.parseEther("1500");
const duration = 365 * 24 * 3600; // 1 year
// first lock
await veRAACToken.connect(users[0]).lock(amount, duration);
const firstLockedPosition = await veRAACToken.getLockPosition(users[0].address);
expect(firstLockedPosition.amount).to.equal(amount);
// second lock
await veRAACToken.connect(users[0]).lock(halfAmount, duration);
const secondLockedPosition = await veRAACToken.getLockPosition(users[0].address);
// expect that after second lock, the total amount is 1500
expect(secondLockedPosition.amount).to.equal(totalAmount);
});

The test will fail with the following output:

Lock Mechanism
should allow users to increase lock amount:
AssertionError: expected 500000000000000000000 to equal 1500000000000000000000.
+ expected - actual
-500000000000000000000
+1500000000000000000000

Impact

Users may loose locked Raac tokens.

Tools Used

Recommendations

Add the following check in lock function:

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();
+ if(_lockState.locks[msg.sender].exists) revert OnlyOneLockPerAddressAllowed();
...
}
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!