Core Contracts

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

TimelockController.sol

Summary

Critical vulnerability in TimelockController where failed emergency actions become permanently locked due to premature state deletion. If an emergency action fails during execution, it becomes impossible to retry the action.

Vulnerability Details

In TimelockController.sol, the contract deletes emergency action state before completing execution:

function executeEmergencyAction(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata calldatas,
bytes32 predecessor,
bytes32 salt
) external payable onlyRole(EMERGENCY_ROLE) nonReentrant {
bytes32 id = hashOperationBatch(targets, values, calldatas, predecessor, salt);
if (!_emergencyActions[id]) revert EmergencyActionNotScheduled(id);
delete _emergencyActions[id];// @audit State deleted before execution
for (uint256 i = 0; i < targets.length; i++) {
(bool success, bytes memory returndata) = targets[i].call{value: values[i]}(calldatas[i]);
if (!success) {
if (returndata.length > 0) {
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
}
revert CallReverted(id, i); // @audit Reverts after state deletion
}
}
emit EmergencyActionExecuted(id);
}

This creates a permanent denial of service condition for emergency actions that fail during execution.

Impact

  1. Emergency actions become permanently unusable if execution fails

  2. Protocol would require redeployment to restore emergency functionality

  3. Risk of fund loss during market stress when emergency functions are critical

  4. No way to retry failed emergency actions

Proof of Concept

The PoC shows that

1. Once an emergency action is scheduled

2. If the execution fails

3. The action's state is deleted before completion

4. Making it impossible to retry the action

5. Effectively locking critical emergency functionality

This creates a serious vulnerability in the protocol's emergency response capabilities. The following test case demonstrates this scenario using a mock target contract that intentionally fails:

const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("TimelockController Emergency Action Vulnerability", function() {
let timelock;
let mockTarget;
let owner;
let emergency;
let proposer;
beforeEach(async function() {
[owner, emergency, proposer] = await ethers.getSigners();
// Deploy mock target that will fail
const MockTarget = await ethers.getContractFactory("MockTarget");
mockTarget = await MockTarget.deploy();
await mockTarget.deployed();
// Deploy TimelockController
const TimelockController = await ethers.getContractFactory("TimelockController");
timelock = await TimelockController.deploy(
2 * 24 * 60 * 60, // 2 days minimum delay
[proposer.address],
[emergency.address],
owner.address
);
await timelock.deployed();
});
it("Should permanently block emergency action after failed execution", async function() {
// Create action parameters
const targets = [mockTarget.address];
const values = [0];
const data = mockTarget.interface.encodeFunctionData("failingFunction", []);
const calldatas = [data];
const ZERO_BYTES32 = ethers.constants.HashZero;
const salt = ethers.utils.formatBytes32String("salt");
// Calculate action ID
const actionId = await timelock.hashOperationBatch(
targets,
values,
calldatas,
ZERO_BYTES32,
salt
);
// Schedule emergency action
await timelock.connect(emergency).scheduleEmergencyAction(actionId);
// Verify action is scheduled
expect(await timelock.isScheduledEmergencyAction(actionId)).to.be.true;
// First attempt - should fail but delete the action
await expect(
timelock.connect(emergency).executeEmergencyAction(
targets,
values,
calldatas,
ZERO_BYTES32,
salt
)
).to.be.revertedWith("MockTarget: intentional failure");
// Action should now be unscheduled due to premature deletion
expect(await timelock.isScheduledEmergencyAction(actionId)).to.be.false;
// Second attempt - should fail because action is deleted
await expect(
timelock.connect(emergency).executeEmergencyAction(
targets,
values,
calldatas,
ZERO_BYTES32,
salt
)
).to.be.revertedWith("EmergencyActionNotScheduled");
});
});
// Mock contract that will fail
contract MockTarget {
function failingFunction() external pure {
revert("MockTarget: intentional failure");
}
}

Tools Used

  • Manual code review

  • Hardhat for testing and POC

  • Ethers.js

Recommended Mitigation

Move state deletion after successful execution:

function executeEmergencyAction(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata calldatas,
bytes32 predecessor,
bytes32 salt
) external payable onlyRole(EMERGENCY_ROLE) nonReentrant {
bytes32 id = hashOperationBatch(targets, values, calldatas, predecessor, salt);
if (!_emergencyActions[id]) revert EmergencyActionNotScheduled(id);
// Execute all calls first
for (uint256 i = 0; i < targets.length; i++) {
(bool success, bytes memory returndata) = targets[i].call{value: values[i]}(calldatas[i]);
if (!success) {
if (returndata.length > 0) {
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
}
revert CallReverted(id, i);
}
}
// Delete state only after successful execution
delete _emergencyActions[id];
emit EmergencyActionExecuted(id);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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