Core Contracts

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

The `setEmergencyPaused()` function is not implemented anywhere in the `BaseGauge` Or `RWAGauge` contract

Summary

The withdraw function in the contract lacks a pause check, allowing users to withdraw staked tokens even when the contract is in an emergency paused state. This undermines the purpose of the emergency pause mechanism, which is designed to halt core functionality during emergencies or vulnerabilities.

and this apply to other Core function that is supposed to be paused when the contract admin calls the setEmergencyPaused() function for an emergency.

Vulnerability Details

Location

Description

The withdraw function does not include a mechanism to check whether the contract is paused. The contract implements an emergency pause feature via the setEmergencyPaused function, which can pause or unpause the contract. However, the withdraw function does not respect this pause state, allowing users to withdraw funds even when the contract is paused.

function setEmergencyPaused(bool paused) external {
if (!hasRole(EMERGENCY_ADMIN, msg.sender)) revert UnauthorizedCaller();
if (paused) {
_pause();
} else {
_unpause();
}
}
function withdraw(uint256 amount) external nonReentrant updateReward(msg.sender) whenNotPaused {
if (amount == 0) revert InvalidAmount();
if (_balances[msg.sender] < amount) revert InsufficientBalance();
_totalSupply -= amount;
_balances[msg.sender] -= amount;
stakingToken.safeTransfer(msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}

Root Cause

The absence of a whenNotPaused modifier or an explicit check for the pause state in the withdraw function allows it to execute even when the contract is paused.

Impact

Financial Impact

  • If the contract is paused due to an emergency (e.g., a discovered vulnerability), users can still withdraw their funds. This could lead to loss of funds or exploitation of the vulnerability.

Security Impact

  • The pause mechanism is a critical safety feature. If it doesn't work as intended, the contract is less secure and more vulnerable to attacks.

Reputation Impact

  • Users and stakeholders may lose trust in the protocol if they discover that the emergency pause mechanism is ineffective.

Tools Used

  • Manual Code Review: The issue was identified through a thorough manual review of the contract code.

  • Foundry: Unit tests was written to verify the behavior of the withdraw function when the contract is paused.

Foundry POC:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/src/Test.sol";
import "../../contracts/core/governance/gauges/GaugeController.sol";
import "../../contracts/core/governance/gauges/RWAGauge.sol";
import "../../contracts/mocks/core/tokens/ERC20Mock.sol";
import "../../contracts/core/tokens/veRAACToken.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract RWAGaugeTest is Test {
GaugeController Controller;
RWAGauge rWAGauge;
ERC20Mock token;
ERC20Mock veraactoken;
address admin;
address user;
address pool;
address public TokenAddress;
uint256 STARTING_BALANCE = 100 ether;
uint256 amount = 10 ether;
function setUp() public {
admin = vm.addr(1);
user = vm.addr(2);
pool = vm.addr(3);
vm.startPrank(admin);
token = new ERC20Mock("ERC20Mock", "ERC");
TokenAddress = address(token);
veraactoken = new ERC20Mock("veraactoken", "VRT");
Controller = new GaugeController(address(veraactoken));
rWAGauge = new RWAGauge(TokenAddress, address(veraactoken), address(Controller));
token.mint(TokenAddress, STARTING_BALANCE);
veraactoken.mint(address(veraactoken), STARTING_BALANCE);
IERC20(TokenAddress).balanceOf(admin);
IERC20(TokenAddress).balanceOf(TokenAddress);
IERC20(TokenAddress).balanceOf(address(this));
vm.stopPrank();
}
function testRWAGauge() public {
token.mint(address(this), amount);
IERC20(address(token)).transfer(address(this), amount);
IERC20(address(token)).approve(address(rWAGauge), amount);
IERC20(address(token)).approve(address(Controller), amount);
IERC20(address(token)).approve(address(veraactoken), amount);
IERC20(address(token)).approve(TokenAddress, amount);
IERC20(TokenAddress).balanceOf(address(this));
veraactoken.mint(address(this), amount);
IERC20(address(veraactoken)).transfer(address(this), amount);
IERC20(address(veraactoken)).approve(address(rWAGauge), amount);
IERC20(address(veraactoken)).approve(address(Controller), amount);
IERC20(address(veraactoken)).approve(address(veraactoken), amount);
IERC20(address(veraactoken)).approve(TokenAddress, amount);
IERC20(TokenAddress).balanceOf(address(this));
uint256 _amount = 1 ether;
rWAGauge.stake(_amount);
IERC20(address(rWAGauge)).balanceOf(address(this));
vm.startPrank(admin);
rWAGauge.setEmergencyPaused(true);
vm.stopPrank();
rWAGauge.withdraw(_amount);
}
}

Foundry Test Response :

├─ [25895] RWAGauge::setEmergencyPaused(true)
│ ├─ emit Paused(account: 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf)
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [18333] RWAGauge::withdraw(1000000000000000000 [1e18])
│ ├─ [569] GaugeController::getGaugeWeight(RWAGauge: [0x51a240271AB8AB9f9a21C82d9a85396b704E164d]) [staticcall]
│ │ └─ ← [Return] 0
│ ├─ emit RewardUpdated(user: RWAGaugeTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], reward: 0)
│ ├─ [3281] ERC20Mock::transfer(RWAGaugeTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 1000000000000000000 [1e18])
│ │ ├─ emit Transfer(from: RWAGauge: [0x51a240271AB8AB9f9a21C82d9a85396b704E164d], to: RWAGaugeTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], value: 1000000000000000000 [1e18])
│ │ └─ ← [Return] true
│ ├─ emit Withdrawn(user: RWAGaugeTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], amount: 1000000000000000000 [1e18])
│ └─ ← [Stop]
└─ ← [Stop]

Recommendations

Add a pause check to the withdraw function using one of the following methods:

Option 1: Use a whenNotPaused Modifier

If the contract inherits from OpenZeppelin's Pausable or a similar contract:

function withdraw(uint256 amount) external nonReentrant updateReward(msg.sender) whenNotPaused {
if (amount == 0) revert InvalidAmount();
if (_balances[msg.sender] < amount) revert InsufficientBalance();
_totalSupply -= amount;
_balances[msg.sender] -= amount;
stakingToken.safeTransfer(msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}

Option 2: Add an Explicit Check

If the contract does not use a modifier, add an explicit check:

function withdraw(uint256 amount) external nonReentrant updateReward(msg.sender) {
require(!paused, "Contract is paused"); // Explicit check
if (amount == 0) revert InvalidAmount();
if (_balances[msg.sender] < amount) revert InsufficientBalance();
_totalSupply -= amount;
_balances[msg.sender] -= amount;
stakingToken.safeTransfer(msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}
Updates

Lead Judging Commences

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

BaseGauge::withdraw, stake, and checkpoint functions lack whenNotPaused modifier, allowing critical state changes even during emergency pause

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

BaseGauge::withdraw, stake, and checkpoint functions lack whenNotPaused modifier, allowing critical state changes even during emergency pause

Support

FAQs

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