Core Contracts

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

Users can destroy their own locks. Severely disrupts the intended functionality of the protocol, makes voting power, total supply, voting state, boost state, checkpoints state, lock duration, etc ambiguous.

Summary

The veRAACToken contract provides various functionalities, including locking, unlocking, increasing, withdrawing, and extending both the locked amount and duration of RAAC tokens. When a user locks RAAC tokens, they receive a proportionate amount of veRAACTokens. This locking mechanism ensures that users can increase the amount and duration of their existing locks but cannot create multiple separate locks. The protocol's documentation explicitly states that users must not be able to create more than one lock, reinforcing this restriction to maintain consistency in the system.

However, due to a bug in the contract, users can bypass this restriction and overwrite or destroy their existing locks by creating a new lock using the veRAACToken::lock function. This flaw allows users to circumvent the intended locking behavior, leading to unintended consequences that compromise the integrity of the protocol. Instead of reinforcing a single lock, the system mistakenly permits the replacement of previously locked tokens, introducing inconsistencies in the contract’s state.

This vulnerability has severe implications for the protocol’s core mechanisms. It disrupts fundamental components such as voting power calculations, the total supply of veRAACTokens, boost states, checkpoint states, and lock durations. The ambiguity in these values creates uncertainty across multiple protocol functionalities, rendering critical processes unreliable.

Furthermore, the flawed locking mechanism undermines key aspects of the protocol, including the rewards distribution mechanism (gauge system), the governance voting system, and historical data tracking. The inability to maintain a consistent lock state leads to an inaccurate representation of voting power and token supply, distorting governance participation and reward allocation. Over time, these inconsistencies erode trust in the protocol and may create opportunities for malicious exploitation.

Vulnerability Details

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();
}
@> // @info: missing check if lock exists already
// 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);
}

On deeper inspection, it was discovered that a struct exists with a member intended to indicate whether a lock exists. However, this Lock struct is not actually used to determine the presence of a lock in the system.

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

Furthermore, this struct is incorporated as a member of the _lockState struct (or library). Under the hood, the LockManager library does not check for the existence of a lock during lock creation; it simply creates a new lock and overwrites any existing lock without performing any validation.

LockManager::createLock: (Out of Scope)

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;
}

Proof of Concept

To demonstrate this vulnerability, the following Proof of Concept (PoC) is provided. The PoC is written using the Foundry tool.

  1. Step 1: Create a Foundry project and place all the contracts in the src directory.

  2. Step 2: Create a test directory and a mocks folder within the src directory (or use an existing mocks folder).

  3. Step 3: Create all necessary mock contracts, if required.

  4. Step 4: Create a test file (with any name) in the test directory.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {veRAACToken} from "../src/core/tokens/veRAACToken.sol";
import {RAACToken} from "../src/core/tokens/RAACToken.sol";
import {TimeWeightedAverage} from "../src/libraries/math/TimeWeightedAverage.sol";
import {LockManager} from "../src/libraries/governance/LockManager.sol";
import {IveRAACToken} from "../src/interfaces/core/tokens/IveRAACToken.sol";
contract VeRAACTokenTest is Test {
veRAACToken veRaacToken;
RAACToken raacToken;
address RAAC_OWNER = makeAddr("RAAC_OWNER");
address RAAC_MINTER = makeAddr("RAAC_MINTER");
uint256 initialRaacSwapTaxRateInBps = 200; // 2%, 10000 - 100%
uint256 initialRaacBurnTaxRateInBps = 150; // 1.5%, 10000 - 100%
address VE_RAAC_OWNER = makeAddr("VE_RAAC_OWNER");
address ALICE = makeAddr("ALICE");
address BOB = makeAddr("BOB");
address CHARLIE = makeAddr("CHARLIE");
address DEVIL = makeAddr("DEVIL");
function setUp() public {
raacToken = new RAACToken(RAAC_OWNER, initialRaacSwapTaxRateInBps, initialRaacBurnTaxRateInBps);
vm.startPrank(VE_RAAC_OWNER);
veRaacToken = new veRAACToken(address(raacToken));
vm.stopPrank();
}
}
  1. Step 5: Add the following test PoC in the test file, after the setUp function.

function testLockFunctionBugAllowsUsersToSelfDestroyTheirLock() public {
uint256 LOCK_AMOUNT = 10_000_000e18;
uint256 LOCK_DURATION = 365 days;
vm.startPrank(RAAC_OWNER);
raacToken.setMinter(RAAC_MINTER);
vm.stopPrank();
// we modified (mutated) some private state variables to public
console.log("total supply max : ", veRaacToken.MAX_TOTAL_SUPPLY());
console.log("total supply : ", veRaacToken.getTotalVotingPower());
vm.startPrank(RAAC_MINTER);
raacToken.mint(ALICE, LOCK_AMOUNT);
vm.stopPrank();
// alice locked her raac tokens to veRAACToken contract and
// gets some voting power as a receipt of veRaacTokens.
vm.startPrank(ALICE);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
console.log("\nafter alice locked her raac tokens...");
console.log("total supply max : ", veRaacToken.MAX_TOTAL_SUPPLY());
console.log("total supply : ", veRaacToken.getTotalVotingPower());
console.log("alice veRaac balance : ", veRaacToken.balanceOf(ALICE));
uint256 aliceCurrentVotingPower = veRaacToken.getVotingPower(ALICE);
console.log("alice current Voting Power: ", aliceCurrentVotingPower);
// now alice wants to increase her voting power and decides to lock her raac token again
// she uses lock function instead of increase function...
vm.startPrank(RAAC_MINTER);
raacToken.mint(ALICE, LOCK_AMOUNT / 2);
vm.stopPrank();
vm.startPrank(ALICE);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT / 2);
veRaacToken.lock(LOCK_AMOUNT / 2, LOCK_DURATION);
vm.stopPrank();
console.log("\nafter alice locked again/increased her raac tokens...");
console.log("total supply max : ", veRaacToken.MAX_TOTAL_SUPPLY());
console.log("total supply : ", veRaacToken.getTotalVotingPower());
console.log("alice veRaac balance : ", veRaacToken.balanceOf(ALICE));
aliceCurrentVotingPower = veRaacToken.getVotingPower(ALICE);
console.log("alice current Voting Power: ", aliceCurrentVotingPower);
assertNotEq(aliceCurrentVotingPower, veRaacToken.balanceOf(ALICE));
assertNotEq(aliceCurrentVotingPower, veRaacToken.getTotalVotingPower());
}
  1. Step 6: To run the test, execute the following commands in your terminal:

forge test --mt testLockFunctionBugAllowsUsersToSelfDestroyTheirLock -vv
  1. Step 7: Review the output. The expected output should indicate that users were successful to overwrite their old locks and this vulnerability creates severe ambiguities in the porotocol functionalities.

[⠒] Compiling...
No files changed, compilation skipped
Ran 1 test for test/VeRAACTokenTest.t.sol:VeRAACTokenTest
[PASS] testLockFunctionBugAllowsUsersToSelfDestroyTheirLock() (gas: 713757)
Logs:
total supply max : 100000000000000000000000000
total supply : 0
after alice locked her raac tokens...
total supply max : 100000000000000000000000000
total supply : 2500000000000000000000000
alice veRaac balance : 2500000000000000000000000
alice current Voting Power: 2500000000000000000000000
after alice locked again/increased her raac tokens...
total supply max : 100000000000000000000000000
total supply : 3750000000000000000000000
alice veRaac balance : 3750000000000000000000000
alice current Voting Power: 1250000000000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.57ms (1.31ms CPU time)
Ran 1 test suite in 19.53ms (3.57ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

As demonstrated, the test confirms that the veRAACToken::lock function hasn't any existence check for already existing locks and this flaw severely disrupts the intended functionality of the protocol.

Impact

  • Inaccurate Lock State Determination:
    The inability to correctly determine whether a lock exists leads to unreliable tracking of users' locked tokens, resulting in erroneous data regarding locked amounts and durations.

  • Disrupted Voting Power Calculations:
    Inaccurate lock states directly affect the computation of voting power, potentially causing users to lose their rightful influence in governance decisions.

  • Compromised Rewards Distribution:
    Since rewards and boost mechanisms rely on accurate lock information, overwritten locks can lead to improper reward allocation, undermining the fairness of the incentive system.

  • Increased Vulnerability to Malicious Exploitation:
    The lack of validation in the LockManager library allows adversaries to deliberately overwrite existing locks, manipulating voting power and reward distribution for malicious gain.

  • Erosion of Trust in the Protocol:
    These inconsistencies and vulnerabilities undermine the integrity of core protocol functionalities, potentially leading to reputational damage and reduced user confidence in the platform.

Tools Used

  • Manual Review

  • Foundry

  • Console Log (foundry)

  • a bit of pinch of mutational testing.

Recommendations

  • Implement Existence Checks: (Out of scope)
    Modify the LockManager library to utilize the lock existence indicator in the Lock struct. Before creating a new lock, the contract should check whether an active lock already exists for the user.

  • Prevent Overwriting of Existing Locks:
    Update the lock creation logic in the veRAACToken contract to either disallow the creation of a new lock if one already exists or enforce that users can only update (increase or extend) their existing lock.

  • Improve Error Handling:
    Introduce clear and informative error messages when a user attempts to create a new lock while an active lock already exists, guiding them to use the appropriate functions (e.g., increase or extend).

One Possible solution is as follows:

veRAACToken::lock:

function lock(uint256 amount, uint256 duration) external nonReentrant whenNotPaused {
+ if (_lockState.locks[msg.sender].exists) {
+ revert LockAlreadyExists();
+ }
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);
}
Updates

Lead Judging Commences

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