Skip to main content

CircuitBreaker

Proposed in CircuitBreaker: Programmable Panic Layer.

An emergency-pause layer for Lido protocol contracts.

NetworkAddress
Mainnet0x6019CB557978296BA3C08a7B73225C0975DFB2F7
Hoodi0x44a5789dFeDa59cD176Ab5709ec2F4829dE4d555

What is CircuitBreaker?​

CircuitBreaker is a single, permanent contract that lets DAO-designated pauser committees instantly pause registered Lido contracts for a bounded duration without waiting for a governance vote. It is the successor to GateSeal: instead of single-use, expiring instances that must be redeployed every year, CircuitBreaker operates indefinitely.

Why use a CircuitBreaker?​

Putting critical Lido components on hold via a DAO vote can take many days. CircuitBreaker provides a way to temporarily pause these contracts immediately while the DAO investigates, deliberates, and executes a decision.

It is operated by committees, multisig accounts authorized to pull the brake in an emergency. Granting a committee unilateral pause authority is non-trivial, so CircuitBreaker has a number of safeguards:

  • Single-use per pausable: a successful pause unregisters the committee from that pausable contract. To pause the same contract again, the pauser must be re-assigned by a full DAO vote. A misbehaving committee can pause only its assigned contracts and only once.
  • Bounded pause duration: the pause has a limited duration controlled by the DAO, i.e. the pauser does not choose the duration when triggering the pause.
  • Pause only: CircuitBreaker holds only the pause role on its registered pausables. The contract cannot resume the pausables, doesn't manage funds, doesn't have a proxy.
  • Liveness via heartbeats: each pauser maintains its own heartbeat. If the heartbeat expires, the pauser can neither pause nor self-prolong authority. This means that unresponsive committees lose authority automatically.
  • Immutable admin: the admin address is set at construction and cannot be changed, eliminating ownership-transfer exploits.

Roles​

  • Admin — an immutable address, the DAO Agent. Configures the registry and controls the pause duration and heartbeat interval.
  • Pauser — a multisig committee assigned to one or more pausables. Can pause any pausable it is registered for, and must periodically call heartbeat() to remain authorized.

Immutable bounds and current values​

CircuitBreaker is deployed with immutable bounds:

  • MIN_PAUSE_DURATION / MAX_PAUSE_DURATION — inclusive lower/upper bounds for pauseDuration.
  • MIN_HEARTBEAT_INTERVAL / MAX_HEARTBEAT_INTERVAL — inclusive lower/upper bounds for heartbeatInterval.

Within these bounds, the admin can adjust pauseDuration and heartbeatInterval at any time without redeployment. Changes to heartbeatInterval apply only to subsequent heartbeats, i.e. already-stored expiries are not retroactively updated.

The deployed parameter sets are:

ParameterMainnetHoodi
MIN_PAUSE_DURATION5 days (432,000 s)60 s
MAX_PAUSE_DURATION60 days (5,184,000 s)30 days (2,592,000 s)
MIN_HEARTBEAT_INTERVAL30 days (2,592,000 s)60 s
MAX_HEARTBEAT_INTERVAL3 years (94,608,000 s)3 years (94,608,000 s)
Initial pauseDuration21 days (1,814,400 s)1 hour (3,600 s)
Initial heartbeatInterval1 year (31,536,000 s)1 year (31,536,000 s)

The mainnet 21-day initial pause duration is sized to cover the worst-case governance timeline: two consecutive Aragon votes (≈10 days), a minimum Dual Governance timelock (4 days), and a 7-day buffer for analysis and coordination. Hoodi uses relaxed bounds appropriate for testnet drills.

Heartbeat mechanism​

Each pauser has its own heartbeat expiry timestamp. The pauser is considered live while their expiry timestamp is in the future. While live, the pauser can pause any of its assigned contracts and can extend its expiry by sending a heartbeat. Once the expiry passes, the pauser is no longer considered live and can neither pause nor extend expiry.

A heartbeat is a drill transaction that updates the caller's heartbeat. An expired pauser cannot revive itself, so the pauser must renew their heartbeat before it expires.

The expiry is also updated on registration and pause:

  • When the DAO assigns a pauser to a pausable, that pauser's expiry is updated, regardless of its previous value.
  • When a pauser is unassigned from its last remaining pausable (either by the DAO or by triggering a pause) its expiry is cleared.
  • A successful pause that leaves the caller with at least one other assigned pausable refreshes the caller's expiry the same way a heartbeat would.

Pause flow​

When a registered, live pauser triggers a pause on one of its assigned pausables, CircuitBreaker:

  1. Unregisters the pauser from that pausable.
  2. Pauses the pausable for the preconfigured pause duration. The pausable is expected to follow the PausableUntil pattern.
  3. Reads back the pausable's state to confirm the pause actually took effect, reverting if it did not.
  4. Updates the caller's heartbeat expiry as described above.

A reentrancy guard prevents a malicious pausable from calling back into CircuitBreaker during this flow to trigger additional pauses.

CircuitBreaker does not verify at registration time that a pausable implements the expected interface or that CircuitBreaker has been granted the pause role on it. These properties can also change later, for example through a proxy upgrade or a role revocation. The DAO is therefore responsible for ensuring the pause role is granted before assigning a pauser.

Covered pausables​

The set of pausables and their assigned pausers is maintained by the DAO. See the deployed-contracts pages for the current registry on each network:

View Methods​

ADMIN()​

Returns the immutable admin address.

function ADMIN() external view returns (address);

MIN_PAUSE_DURATION() / MAX_PAUSE_DURATION()​

Inclusive lower and upper bounds, in seconds, for pauseDuration. Set at deployment, immutable thereafter.

function MIN_PAUSE_DURATION() external view returns (uint256);
function MAX_PAUSE_DURATION() external view returns (uint256);

MIN_HEARTBEAT_INTERVAL() / MAX_HEARTBEAT_INTERVAL()​

Inclusive lower and upper bounds, in seconds, for heartbeatInterval. Set at deployment, immutable thereafter.

function MIN_HEARTBEAT_INTERVAL() external view returns (uint256);
function MAX_HEARTBEAT_INTERVAL() external view returns (uint256);

pauseDuration()​

Current pause duration, in seconds, applied to a pausable on a successful trigger.

function pauseDuration() external view returns (uint256);

heartbeatInterval()​

Current heartbeat interval, in seconds. The window after a heartbeat during which the pauser remains authorized.

function heartbeatInterval() external view returns (uint256);

heartbeatExpiry()​

Returns the timestamp after which the given pauser is no longer authorized to heartbeat or pause.

function heartbeatExpiry(address pauser) external view returns (uint256);

Parameters​

NameTypeDescription
pauseraddressPauser address to look up.

getPauser()​

Returns the pauser currently registered for a pausable, or the zero address if none.

function getPauser(address _pausable) external view returns (address);

Parameters​

NameTypeDescription
_pausableaddressPausable contract address.

getPausables()​

Returns all pausable addresses currently registered.

function getPausables() external view returns (address[] memory);

getPausableCount()​

Returns the number of pausables assigned to a pauser.

function getPausableCount(address _pauser) external view returns (uint256);

Parameters​

NameTypeDescription
_pauseraddressPauser address.

isPauserLive()​

Returns whether the pauser's heartbeat has not expired.

function isPauserLive(address _pauser) external view returns (bool);

Parameters​

NameTypeDescription
_pauseraddressPauser address.

Returns true when block.timestamp < heartbeatExpiry[_pauser].

Write Methods​

Admin methods​

The following methods can be called only by ADMIN. They revert with SenderNotAdmin otherwise.

setPauseDuration()​

Sets the pause duration applied on subsequent triggers. The new value takes effect immediately for any pauses called afterward.

function setPauseDuration(uint256 _newPauseDuration) external;

Parameters​

NameTypeDescription
_newPauseDurationuint256New pause duration, in seconds.
note

Reverts if any of the following is true:

  • caller is not ADMIN (SenderNotAdmin)
  • _newPauseDuration < MIN_PAUSE_DURATION (PauseDurationBelowMin)
  • _newPauseDuration > MAX_PAUSE_DURATION (PauseDurationAboveMax)

Emits PauseDurationUpdated(previousPauseDuration, newPauseDuration).

setHeartbeatInterval()​

Sets the heartbeat interval pausers must maintain to remain authorized. The new value applies only to subsequent heartbeats and registrations; already-stored heartbeatExpiry values are not changed retroactively.

function setHeartbeatInterval(uint256 _newHeartbeatInterval) external;

Parameters​

NameTypeDescription
_newHeartbeatIntervaluint256New heartbeat interval, in seconds.
note

Reverts if any of the following is true:

  • caller is not ADMIN (SenderNotAdmin)
  • _newHeartbeatInterval < MIN_HEARTBEAT_INTERVAL (HeartbeatIntervalBelowMin)
  • _newHeartbeatInterval > MAX_HEARTBEAT_INTERVAL (HeartbeatIntervalAboveMax)

Emits HeartbeatIntervalUpdated(previousHeartbeatInterval, newHeartbeatInterval).

registerPauser()​

Registers, replaces, or unregisters a pauser for a pausable.

  • The previous pauser, if any, is overwritten. If they are left with zero remaining pausables, their heartbeatExpiry is cleared to 0.
  • The new pauser's heartbeatExpiry is set to block.timestamp + heartbeatInterval (extending or initializing it).
  • Passing address(0) as _newPauser unregisters the pausable's current pauser.
function registerPauser(address _pausable, address _newPauser) external;

Parameters​

NameTypeDescription
_pausableaddressPausable contract address.
_newPauseraddressNew pauser address. Zero unregisters the current pauser, if any.
note
  • Reverts if caller is not ADMIN (SenderNotAdmin).
  • Does not verify that CircuitBreaker holds the pause role on _pausable, or that _pausable implements IPausable. The DAO is responsible for ensuring these invariants when assigning pausers.

Emits HeartbeatUpdated for the previous pauser (if their expiry was cleared) and for the new pauser.

Pauser methods​

heartbeat()​

Records a liveness proof, extending the caller's heartbeatExpiry to block.timestamp + heartbeatInterval.

function heartbeat() external;
note

Reverts if any of the following is true:

  • caller is not registered as a pauser for any pausable (SenderNotPauser)
  • caller's heartbeat has already expired (HeartbeatExpired) — a lapsed pauser cannot self-renew; the DAO must explicitly re-register them

Emits HeartbeatUpdated(pauser, newHeartbeatExpiry).

pause()​

Pauses a registered pausable for the current pauseDuration. Single-use: the caller is unregistered from this pausable on success.

function pause(address _pausable) external;

The target must implement the minimal IPausable interface that CircuitBreaker calls into:

interface IPausableUntil {
function isPaused() external view returns (bool);
function pauseFor(uint256 _duration) external;
}

Parameters​

NameTypeDescription
_pausableaddressPausable contract to pause.

The execution flow is:

  1. Verify msg.sender is the registered pauser of _pausable and is live.
  2. Unregister msg.sender from _pausable.
  3. Call IPausable(_pausable).pauseFor(pauseDuration).
  4. Verify IPausable(_pausable).isPaused() is true.
  5. Update the caller's heartbeatExpiry: extended to block.timestamp + heartbeatInterval if any other pausables are still assigned to them, or cleared to 0 otherwise.
note

Reverts if any of the following is true:

  • caller is not the registered pauser of _pausable (SenderNotPauser)
  • caller's heartbeat has expired (HeartbeatExpired)
  • the target does not report itself paused after pauseFor() call (PauseFailed)
  • the call reentered (ReentrantCall)

Emits PauseTriggered(pausable, pauser, pauseDuration) and HeartbeatUpdated(pauser, newHeartbeatExpiry).

Events​

event CircuitBreakerInitialized(
address indexed admin,
uint256 minPauseDuration,
uint256 maxPauseDuration,
uint256 minHeartbeatInterval,
uint256 maxHeartbeatInterval
);

Emitted once at construction with the immutable admin and bounds.

event PauseDurationUpdated(uint256 previousPauseDuration, uint256 newPauseDuration);

Emitted on setPauseDuration and once at construction for the initial value.

event HeartbeatIntervalUpdated(uint256 previousHeartbeatInterval, uint256 newHeartbeatInterval);

Emitted on setHeartbeatInterval and once at construction for the initial value.

event HeartbeatUpdated(address indexed pauser, uint256 newHeartbeatExpiry);

Emitted whenever a pauser's heartbeat expiry changes — on heartbeat(), pause(), and registerPauser().

event PauseTriggered(address indexed pausable, address indexed pauser, uint256 pauseDuration);

Emitted on a successful pause().