Core Contracts

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

Emergency Timelock Bypass: No Enforced 1-Day Delay for Emergency Actions

Summary

The TimelockController is designed to enforce a delay for governance actions, including emergency actions that are supposed to wait 1 day (EMERGENCY_DELAY) before execution. However, the code allows instant emergency execution without enforcing any on-chain delay. This contradicts the documentation and can enable a privileged user with the EMERGENCY_ROLE to bypass the expected 1-day timelock, pushing protocol changes immediately.

Documentation on TimelockController, section Usage, states:

Emergency actions have 1-day delay

Vulnerability Details

In the TimelockController contract, the two relevant functions are:

function scheduleEmergencyAction(bytes32 id) external onlyRole(EMERGENCY_ROLE) {
_emergencyActions[id] = true;
emit EmergencyActionScheduled(id, block.timestamp);
}
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);
-> // @audit No timestamp or delay check here — can run immediately
delete _emergencyActions[id];
for (uint256 i = 0; i < targets.length; i++) {
(bool success, bytes memory returndata) = targets[i].call{value: values[i]}(calldatas[i]);
if (!success) {
revert CallReverted(id, i);
}
}
emit EmergencyActionExecuted(id);
}

Suppose the protocol states that emergency actions require a 1-day delay, giving token holders or the broader community time to notice and respond to potential drastic changes. Instead, anyone controlling the EMERGENCY_ROLE can do the following instantly in consecutive transactions.

Impact

  • Timelock Bypass: The core premise of a timelock is giving stakeholders a buffer period to react. This vulnerability breaks that assumption for emergency actions.

  • Immediate Execution Risk: If the EMERGENCY_ROLE key is compromised or used maliciously, the attacker can push arbitrary changes instantly, potentially seizing funds or breaking the protocol.

  • Documentation Mismatch: The protocol documents a 1-day delay for emergency operations, yet the code offers none. This misleads users and governance participants who believe they have 24 hours to respond.

Tools Used

Manual Review, Hardhat.

PoC

This test can be added at the end of the describe block: "Emergency Actions", in the file "test/unit/core/governance/proposals/TimelockController.test.js".

it("should demonstrate that emergency actions can be executed immediately, bypassing the documented 1-day delay", async () => {
const targets = [await testTarget.getAddress()];
const values = [0];
const calldatas = [testTarget.interface.encodeFunctionData("setValue", [999])];
// Hash the operation for identification
const operationId = await timelock.hashOperationBatch(
targets,
values,
calldatas,
ethers.ZeroHash,
ethers.ZeroHash
);
// 1) Schedule the emergency action
await timelock.connect(owner).scheduleEmergencyAction(operationId);
// 2) Immediately execute the emergency action (no time increase)
await expect(
timelock.connect(owner).executeEmergencyAction(
targets,
values,
calldatas,
ethers.ZeroHash,
ethers.ZeroHash
)
).to.emit(timelock, "EmergencyActionExecuted")
.withArgs(operationId);
// Verify the target call took effect right away
expect(await testTarget.value()).to.equal(999);
// This proves there's no enforced 1-day wait in the contract,
// despite the documentation suggesting an EMERGENCY_DELAY is required.
});

Recommendations

Add a Timelock Check for Emergency Actions:

  • Include a timestamp in scheduleEmergencyAction:

    function scheduleEmergencyAction(bytes32 id) external onlyRole(EMERGENCY_ROLE) {
    _emergencyActions[id] = true;
    + emergencyActionScheduledAt[id] = block.timestamp;
    emit EmergencyActionScheduled(id, block.timestamp);
    }
  • In executeEmergencyAction, require:

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);
+ require(
+ block.timestamp >= _emergencyActionScheduledAt[id] + EMERGENCY_DELAY,
+ "Emergency delay not met"
+ );
... rest of code
Updates

Lead Judging Commences

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

TimelockController emergency actions bypass timelock by not enforcing EMERGENCY_DELAY, allowing immediate execution

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

TimelockController emergency actions bypass timelock by not enforcing EMERGENCY_DELAY, allowing immediate execution

Support

FAQs

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