Core Contracts

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

It is possible to lock tokens beyond the `MAX_LOCK_DURATION`

Summary

In veRAACToken, it is possible to lock tokens beyond the MAX_LOCK_DURATION

Vunerability Details

When creating a lock position in veRAACToken::lock, we ensure that the duration is always less than MAX_LOCK_DURATION

File: contracts/core/tokens/veRAACToken.sol#L212-L244
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();
// @audit notice the duration must be less than or equal to MAX_LOCK_DURATION
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);
}

To extend a locks duration, we make use of the veRAACToken::extend function.
This function then calls the LockManager::extendLock function which verifies that, the new duration is not beyond the maxLockDuration

function extendLock(
LockState storage state,
address user,
uint256 extensionDuration
) internal returns (uint256 newEnd) {
Lock storage lock = state.locks[user];
if (!lock.exists) revert LockNotFound();
if (lock.end <= block.timestamp) revert LockExpired();
// Calculate remaining duration from current lock
uint256 remainingDuration = lock.end - block.timestamp;
// Calculate total new duration (remaining + extension)
uint256 totalNewDuration = remainingDuration + extensionDuration;
// Check if total duration exceeds max lock duration
if (totalNewDuration > state.maxLockDuration) revert InvalidLockDuration();
// Calculate new end time
newEnd = block.timestamp + totalNewDuration;
// Update lock end time
lock.end = newEnd;
emit LockExtended(user, newEnd);
return newEnd;
}

However, it fails to account for the fact that, said lock might have already existed for a duration such that, (newEnd - lock.start) might be greater than MAX_LOCK_DURATION
in which case, it should revert. This means that, it's technically technically possible for one to create a lock with an effective duration of more than 4 years.

Proof of Concept

  • At timestamp T, create a lock with a duration of MAX_LOCK_DURATION. Notice the start of the lock is T

  • At timestamp T + MAX_LOCK_DURATION - 1, extend the duration of the lock by an additioanl duration of MAX_LOCK_DURATION - 2

  • totalNewDuration of the lock will be (T + MAX_LOCK_DURATION) - (T + MAX_LOCK_DURATION - 1) + (MAX_LOCK_DURATION - 2) or more simplified MAX_LOCK_DURATION - 1

  • Thus newEnd of the lock position at timestmap T + MAX_LOCK_DURATION - 1 after the call to extend the duration will be T + (2 * MAX_LOCK_DURATION) - 2 which is greater than maxLockDuration

  • Thus, assuming this lock is not extended anymore, this lock will have a start of T and an end of T + (2 * MAX_LOCK_DURATION) - 2

  • meaning that, the total duration of the lock has exceeded the maximum desired duration by the protocol of MAX_LOCK_DURATION

it("should extend the end to a date with an effective duration beyond MAX_LOCK_DURATION", async () => {
const amount = ethers.parseEther("1000");
const initialDuration = MAX_LOCK_DURATION;
// First create a lock
await raacToken.mint(users[0].address, amount);
await raacToken.connect(users[0]).approve(await veRAACToken.getAddress(), amount);
await veRAACToken.connect(users[0]).lock(amount, initialDuration);
await time.increase(MAX_LOCK_DURATION - 1);
// Try to extend the lock duration
await expect(veRAACToken.connect(users[0]).extend(MAX_LOCK_DURATION - 2))
.to.emit(veRAACToken, "LockExtended")
});

Copy the above code in test/unit/core/tokens/veRAACToken.test.js and run npx hardhat test test/unit/core/tokens/veRAACToken.test.js in the terminal.

Impact

Users can indefinitely extend their locked positions thereby bypassing the MAX_LOCK_DURATION constraint set by the protocol thereby hoarding the voting power for themselves given that, there's a limit to how much can be locked in the entire protocol

see here: https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/tokens/veRAACToken.sol#L215

Tools Used

Manual review.

Recommendations

Track when the lock got created in the lock.start property of the lock and implement the below changes

// File LockManager.sol
struct Lock {
uint256 amount; // Amount of tokens locked
+ uint256 start; // Timestamp at which the lock got created
uint256 end; // Timestamp when lock expires
bool exists; // Flag indicating if lock exists
}
function extendLock(
LockState storage state,
address user,
uint256 extensionDuration
) internal returns (uint256 newEnd) {
Lock storage lock = state.locks[user];
if (!lock.exists) revert LockNotFound();
if (lock.end <= block.timestamp) revert LockExpired();
// Calculate remaining duration from current lock
uint256 remainingDuration = lock.end - block.timestamp;
// Calculate total new duration (remaining + extension)
uint256 totalNewDuration = remainingDuration + extensionDuration;
// Check if total duration exceeds max lock duration
- if (totalNewDuration > state.maxLockDuration) revert InvalidLockDuration();
+ if (lock.start - block.timestmap + totalNewDuration > state.maxLockDuration) revert InvalidLockDuration();
// Calculate new end time
newEnd = block.timestamp + totalNewDuration;
// Update lock end time
lock.end = newEnd;
emit LockExtended(user, newEnd);
return newEnd;
}
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,
+ start: block.timestamp,
end: end,
exists: true
});
state.totalLocked += amount;
emit LockCreated(user, amount, end);
return end;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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