Core Contracts

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

Lack of duplicate lock and total locked amount verification in `veRAACToken::lock`

Summary

The veRAACToken::lock function does not check whether a user already has an existing lock before allowing a new one. Additionally, it does not verify the total locked amount against the global maximum (MAX_TOTAL_LOCKED_AMOUNT). This oversight allows users to initiate multiple lock operations and potentially exceed the total lockable supply, leading to unintended behavior in the protocol's lock management.

Vulnerability Details

The veRAACToken::lock function is responsible for locking RAAC tokens and minting veRAAC tokens as voting power. However, it does not verify whether the caller already has an active lock. The contract's lock management structure (LockState) contains an exists flag within the Lock struct, which should be checked before creating a new lock. Since this verification is missing, users can repeatedly call lock, overriding previous locks and creating inconsistencies in the system.

Additionally, the function does not check whether the total amount of locked tokens exceeds the global limit:

/**
* @notice Maximum total amount that can be locked globally
*/
uint256 public constant MAX_TOTAL_LOCKED_AMOUNT = 1_000_000_000e18; // 1B

Without verifying this limit, the contract could allow more tokens to be locked than intended, leading to economic instability.

Affected Code in veRAACToken

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();
// @audit-issue: Does not verify if the user already has an existing lock
// @audit-issue: Does not verify if total locked amount exceeds MAX_TOTAL_LOCKED_AMOUNT
raacToken.safeTransferFrom(msg.sender, address(this), amount);
uint256 unlockTime = block.timestamp + duration;
_lockState.createLock(msg.sender, amount, duration);
_updateBoostState(msg.sender, amount);
(int128 bias, int128 slope) = _votingState.calculateAndUpdatePower(
msg.sender,
amount,
unlockTime
);
uint256 newPower = uint256(uint128(bias));
_checkpointState.writeCheckpoint(msg.sender, newPower);
_mint(msg.sender, newPower);
emit LockCreated(msg.sender, amount, unlockTime);
}

Code Reference: https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/tokens/veRAACToken.sol#L212

Lock State Structure

struct Lock {
uint256 amount; // Amount of tokens locked
uint256 end; // Timestamp when lock expires
bool exists; // Flag indicating if lock exists
}

Since the exists flag is never checked, users can initiate multiple locks without restrictions. Additionally, MAX_TOTAL_LOCKED_AMOUNT is never enforced, allowing the system to exceed intended limits.

Steps to Reproduce

  1. A user locks 100e18 RAAC tokens for 1 year.

  2. The user then locks 500e18 RAAC tokens for 2 years.

  3. The contract does not prevent the second lock, overriding the first lock's details without proper handling.

  4. Multiple users can continue locking until the total amount exceeds MAX_TOTAL_LOCKED_AMOUNT, causing system imbalance.

Proof-of-concept

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import {Test, console} from "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "src/core/tokens/RAACToken.sol";
import "src/core/tokens/veRAACToken.sol";
import "src/interfaces/core/tokens/IveRAACToken.sol";
contract veRAACTokenLockedMoreThanOnceTest is Test {
RAACToken internal raac;
veRAACToken internal veRAAC;
address internal minter;
address internal user;
function setUp() public {
address initialOwner = address(this);
uint256 initialSwapTaxRate = 0;
uint256 initialBurnTaxRate = 0;
raac = new RAACToken(initialOwner, initialSwapTaxRate, initialBurnTaxRate);
veRAAC = new veRAACToken(address(raac));
minter = makeAddr("minter");
user = makeAddr("user");
raac.setMinter(minter);
vm.prank(minter);
raac.mint(user, 1000e18);
}
function testExploit() public {
vm.prank(user);
raac.approve(address(veRAAC), type(uint256).max);
vm.startPrank(user);
veRAAC.lock(100e18, 365 days);
IveRAACToken.LockPosition memory position;
position = veRAAC.getLockPosition(user);
console.log("Locked Amount: ", position.amount); // 100000000000000000000
console.log("Locked End: ", position.end); // 31536001
console.log("Locked Power: ", position.power); // 25000000000000000000
// Lock twice
vm.startPrank(user);
veRAAC.lock(500e18, 365 days * 2);
position = veRAAC.getLockPosition(user);
console.log("Locked Amount: ", position.amount); // 500000000000000000000
console.log("Locked End: ", position.end); // 63072001
console.log("Locked Power: ", position.power); // 275000000000000000000
}
}

Impact

  • Inconsistent Locking Behavior: The protocol assumes that a user can have only one active lock, but multiple locks can exist without merging or rejecting the new one.

  • Incorrect Voting Power Calculation: Since veRAAC tokens represent voting power, multiple locks may cause incorrect voting power distribution.

  • Total Lock Limit Breach: Exceeding MAX_TOTAL_LOCKED_AMOUNT can lead to unforeseen economic issues within the protocol.

Tools Used

Manual Review

Recommendations

Modify lock function to ensure that a user cannot create a new lock if one already exists and verify the total locked amount before proceeding.

Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

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

`veRAACToken::lock` function doesn't check MAX_TOTAL_LOCKED_AMOUNT

Support

FAQs

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