Summary
The TimelockController.sol
is designed to manage time-delayed execution of governance proposals with role-based access control.
However, the contract has an implementation flaw in the emergency action scheduling function,
which does not enforce the required EMERGENCY_DELAY
as stated in the documentation.
Vulnerability Details
Missing Delay Enforcement for Emergency Actions
The scheduleEmergencyAction() allows emergency actions to be scheduled without enforcing the EMERGENCY_DELAY
of 1 day, as specified in EMERGENCY_DELAY and the contract documentation.
Emergency actions have 1-day delay
This bypasses the intended delay mechanism, allowing immediate execution of emergency actions.
The function should store the scheduled timestamp and enforce a minimum delay before allowing execution, similar to how scheduleBatch() correctly enforces delays.
scheduleEmergencyAction()
immediately marks the emergency action as scheduled, allowing execution without a delay.
Impact
The absence of a delay for emergency actions could enable the EMERGENCY_ROLE
to bypass the intended waiting period, executing actions immediately.
This could lead to governance actions being executed without adequate oversight or the intended security buffer.
Tools Used
Recommendations
here is a proper implementation that will help fixing this issue:
Add the following struct to ITimelockController.sol
:
* @notice Struct containing emergency action execution details
* @param timestamp Time when operation can be executed
* @param executed Whether operation has been executed
*/
struct EmergencyAction {
uint64 timestamp;
bool executed;
}
Modify the _emergencyActions
mapping to point to the new added struct:
- mapping(bytes32 => bool) private _emergencyActions;
+ mapping(bytes32 => EmergencyAction ) private _emergencyActions;
Implement scheduleEmergencyAction()
as follow:
* @notice Schedules an emergency action
* @dev Only callable by addresses with EMERGENCY_ROLE
* @param id Operation ID for the emergency action
*/
function scheduleEmergencyAction(bytes32 id) external onlyRole(EMERGENCY_ROLE) {
+ uint256 timestamp = block.timestamp + EMERGENCY_DELAY;
+ _emergencyActions[id] = EmergencyAction({
+ timestamp: timestamp.toUint64(),
+ executed: false
+ });
- _emergencyActions[id] = true;
emit EmergencyActionScheduled(id, block.timestamp);
}
Implement executeEmergencyAction()
as follow:
* @notice Executes an emergency action
* @dev Only callable by addresses with EMERGENCY_ROLE
* @param targets Target addresses for emergency calls
* @param values ETH values for emergency calls
* @param calldatas Calldata for emergency calls
* @param predecessor ID of operation that must be executed before
* @param salt Random value for operation ID
*/
function executeEmergencyAction(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata calldatas,
bytes32 predecessor,
bytes32 salt
) external payable onlyRole(EMERGENCY_ROLE) nonReentrant {
+
+ EmergencyAction storage action = _emergencyActions[id];
+ if (action.timestamp == 0) revert EmergencyActionNotFound(id);
+ if (action.executed) revert EmergencyActionAlreadyExecuted(id);
+
+ if (block.timestamp < action.timestamp) revert EmergencyActionNotReady(id);
bytes32 id = hashOperationBatch(targets, values, calldatas, predecessor, salt);
if (!_emergencyActions[id]) revert EmergencyActionNotScheduled(id);
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) {
if (returndata.length > 0) {
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
}
revert CallReverted(id, i);
}
}
emit EmergencyActionExecuted(id);
}