Core Contracts

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

veRAACToken::extend() allows users to keep voting power indefinitely

Summary

A flaw in the LockManager::extendLock function allows users to lock tokens indefinitely, bypassing the MAX_LOCK_DURATION constraint and retaining voting power forever.

Vunerability Details

When locking RAAC tokens, users should not be able to lock for a duration beyond MAX_LOCK_DURATION as shows the check in veRAACToken.sol#L216

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();
@-> if (duration < MIN_LOCK_DURATION || duration > MAX_LOCK_DURATION)
revert InvalidLockDuration();
// ...The rest of the function...
}

The protocol allows users to extend the duration of their lock position using the veRAACToken::extend function which invokes the LockManager::extendLock function which increases the lock duratio without changing amount.

File: contracts/libraries/governance/LockManager.sol#L180-L205
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;
}

The issue here is that the the total duration check does not take into account the fact that the lock position may have already existed for some time. This can result in the overall duration of the lock position (new expiry time - lock creation time) exceeding MAX_LOCK_DURATION and the transaction not reverting in such a case.

Impact

Users can extend the duration of their locks indefinitely and retain their voting powers forever.

Proof of Concept

Copy the following code in test/unit/core/tokens/veRAACToken.test.js.

it.only("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;
await veRAACToken.connect(users[0]).lock(amount, initialDuration);
await time.increase(MAX_LOCK_DURATION - 1);
await expect(veRAACToken.connect(users[0]).extend(MAX_LOCK_DURATION - 2))
.to.emit(veRAACToken, "LockExtended")
});

Run npx hardhat test test/unit/core/tokens/veRAACToken.test.js in the terminal.

Tools Used

Manual review.

Recommendations

Consider storing the time at which the lock has been created and making the following changes where it is needed in LockManager.sol

File: contracts/libraries/governance/LockManager.sol
struct Lock {
uint256 amount; // Amount of tokens locked
+ uint256 start; // Timestamp at which lock is created
uint256 end; // Timestamp when lock expires
bool exists; // Flag indicating if lock exists
}
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;
}
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;
}
Updates

Lead Judging Commences

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

Support

FAQs

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

Give us feedback!