A version from 2024-04-04.
Dual Governance (DG) is a governance subsystem that sits between the Lido DAO, represented by various voting systems, and the protocol contracts it manages. It protects protocol users from hostile actions by the DAO by allowing to cooperate and block any in-scope governance decision until either the DAO cancels this decision or users' (w)stETH is completely withdrawn to ETH.
This document provides the system description on the code architecture level. A detailed description on the mechanism level can be found in the Dual Governance mechanism design document which should be considered an integral part of this specification.
The system is composed of the following main contracts:
DualGovernance.sol
is a singleton that provides an interface for submitting governance proposals and scheduling their execution, as well as managing the list of supported proposers (DAO voting systems). Implements a state machine tracking the current global governance state which, in turn, determines whether proposal submission and execution is currently allowed.EmergencyProtectedTimelock.sol
is a singleton that stores submitted proposals and provides an interface for their execution. In addition, it implements an optional temporary protection from a zero-day vulnerability in the dual governance contracts following the initial deployment or upgrade of the system. The protection is implemented as a timelock on proposal execution combined with two emergency committees that have the right to cooperate and disable the dual governance.Executor.sol
contract instances make calls resulting from governance proposals' execution. Every protocol permission or role protected by the DG, as well as the permission to manage this role/permission, should be assigned exclusively to one of the instances of this contract (in contrast with being assigned directly to a DAO voting system).Escrow.sol
is a contract that can hold stETH, wstETH, withdrawal NFTs, and plain ETH. It can exist in two states, each serving a different purpose: either an oracle for users' opposition to DAO proposals or an immutable and ungoverned accumulator for the ETH withdrawn as a result of the rage quit.GateSealBreaker.sol
is a singleton that allows anyone to unpause the protocol contracts that were put into an emergency pause by the GateSeal emergency protection mechanism, given that the minimum pause duration has passed and that the DAO execution is not currently blocked by the DG system.The system supports multiple DAO voting systems, represented in the dual governance as proposers. A proposer is an address that has the right to submit sets of EVM calls (proposals) to be made by a dual governance's executor contract. Each proposer has a single associated executor, though multiple proposers can share the same executor, so the system supports multiple executors and the relation between proposers and executors is many-to-one.
The general proposal flow is the following:
ExecutorCall
structs) to be issued by the proposer's associated executor contract, by calling the DualGovernance.submitProposal
function.DualGovernance.scheduleProposal
function, with the execution flow that follows being dependent on the deployment mode.Each submitted proposal requires a minimum timelock before it can be scheduled for execution.
At any time, including while a proposal's timelock is lasting, stakers can signal their opposition to the DAO by locking their (w)stETH or withdrawal NFTs (wNFTs) into the signalling escrow contract. If the opposition exceeds some minimum threshold, the global governance state gets changed, blocking any DAO execution and thus effectively extending the timelock of all pending (i.e. submitted but not scheduled for execution) proposals.
At any time, the DAO can cancel all pending proposals by calling the DualGovernance.cancelAllPendingProposals
function.
By the time the dynamic timelock described above elapses, one of the following outcomes is possible:
The proposal execution flow comes after the dynamic timelock elapses and the proposal is scheduled for execution. The system can function in two deployment modes which affect the flow.
In the regular deployment mode, the emergency protection delay is set to zero and all calls from scheduled proposals are immediately executable by anyone via calling the EmergencyProtectedTimelock.execute
function.
The protected deployment mode is a temporary mode designed to be active during an initial period after the deployment or upgrade of the DG contracts. In this mode, scheduled proposals cannot be executed immediately; instead, before calling EmergencyProtectedTimelock.execute
, one has to wait until an emergency protection timelock elapses since the proposal scheduling time.
In this mode, an emergency activation committee has the one-off and time-limited right to activate an adversarial emergency mode if they see a scheduled proposal that was created or altered due to a vulnerability in the DG contracts or if governance execution is prevented by such a vulnerability. Once the emergency mode is activated, the emergency activation committee is disabled, i.e. loses the ability to activate the emergency mode again. If the emergency activation committee doesn't activate the emergency mode within the duration of the emergency protection duration since the committee was configured by the DAO, it gets automatically disabled as well.
The emergency mode lasts up to the emergency mode max duration counting from the moment of its activation. While it's active, 1) only the emergency execution committee has the right to execute scheduled proposals, and 2) the same committee has the one-off right to disable the DG subsystem, i.e. disconnect executor contracts from the DG contracts and reconnect them to the Lido DAO Voting/Agent contract. The latter also disables the emergency mode and the emergency execution committee, so any proposal can be executed by the DAO without cooperation from any other actors.
If the emergency execution committee doesn't disable the DG until the emergency mode max duration elapses, anyone gets the right to deactivate the emergency mode, switching the system back to the protected mode and disabling the emergency committee.
Note: the protected deployment mode and emergency mode are only designed to protect from a vulnerability in the DG contracts and assume the honest and operational DAO. The system is not designed to handle a situation when there's a vulnerability in the DG contracts AND the DAO is captured/malicious or otherwise dysfunctional.
The DG system implements a state machine tracking the global governance state defining which governance actions are currently possible. The state is global since it affects all non-executed proposals and all system actors.
The state machine is specified in the Dual Governance mechanism design document. The possible states are:
Normal
allows proposal submission and scheduling for execution.VetoSignalling
only allows proposal submission.
VetoSignallingDeactivation
sub-state (doesn't deactivate the parent state upon entry) doesn't allow proposal submission or scheduling for execution.VetoCooldown
only allows scheduling already submitted proposals for execution.RageQuit
only allows proposal submission.Possible state transitions:
Normal
→ VetoSignalling
VetoSignalling
→ RageQuit
VetoSignallingDeactivation
sub-state entry and exit (while the parent VetoSignalling
state is active)VetoSignallingDeactivation
→ VetoCooldown
VetoCooldown
→ Normal
VetoCooldown
→ VetoSignalling
RageQuit
→ VetoCooldown
RageQuit
→ VetoSignalling
These transitions are enabled by three processes (see the mechanism design document for more details):
RageQuit
state);Rage quit is a global process of withdrawing stETH and wstETH locked in the signalling escrow and waiting until all these withdrawals, as well as any withdrawals represented by withdrawal NFTs that were locked into the signalling escrow prior to the process started, are finished.
In the governance state machine, the rage quit process is represented by the RageQuit
global state. While this state is active, no proposal can be scheduled for execution. Thus, rage quit contributes to dynamic timelocks of all pending proposals.
At any time, only one instance of the rage quit process can be active.
From the stakers' point of view, opposition to the DAO and the rage quit process can be described by the following diagram:
The mechanism design allows for a deadlock where the system is stuck in the RageQuit
state while protocol withdrawals are paused or dysfunctional and require a DAO vote to resume, and includes a third-party arbiter Tiebreaker committee for resolving it.
The committee gains the power to bypass the DG dynamic timelock and execute pending proposals under the specific conditions of the deadlock. The detailed Tiebreaker mechanism design can be found in the Dual Governance mechanism design overview document.
The Tiebreaker committee is represented in the system by its address which can be configured via the admin executor calling the DualGovernance.setTiebreakerCommittee
function.
While the deadlock conditions are met, the tiebreaker committee address is allowed to approve execution of any pending proposal by calling DualGovernance.tiebreakerApproveProposal
so that its execution can be scheduled after the tiebreaker execution timelock passes by calling DualGovernance.tiebreakerScheduleProposal
.
The dual governance system supports a set of administrative actions, including:
Each of these actions can only be performed by a designated admin executor contract (set by a configuration option), meaning that:
Encodes an EVM call from an executor contract to the target
address with the specified value
and the calldata being set to payload
.
The main entry point to the dual governance system.
Escrow
contract instances. Tracks the current signalling escrow.This contract is a singleton, meaning that any DG deployment includes exectly one instance of this contract.
Encodes the current global governance state, affecting the set of actions allowed for each of the system's actors.
Instructs the EmergencyProtectedTimelock
singleton instance to register a new governance proposal composed of one or more EVM calls
to be made by an executor contract currently associated with the proposer address calling this function. Starts a dynamic timelock on scheduling the proposal for execution.
See: EmergencyProtectedTimelock.submit
.
The id of the successfully registered proposal.
Normal
, VetoSignalling
, RageQuit
.Triggers a transition of the current governance state (if one is possible) before checking the preconditions.
Instructs the EmergencyProtectedTimelock
singleton instance to schedule the proposal with id proposalId
for execution.
Normal
or VetoCooldown
.Triggers a transition of the current governance state (if one is possible) before checking the preconditions.
Marks the proposal with id proposalId
as approved by the Tiebreaker committee, given that the DG system is in a deadlock.
Triggers a transition of the current governance state (if one is possible) before checking the preconditions.
Instructs the EmergencyProtectedTimelock
singleton instance to schedule the proposal with the id proposalId
for execution, bypassing the proposal dynamic timelock and given that the proposal was previously approved by the Tiebreaker committee and that the tiebreaker execution timelock has elapsed.
TIEBREAKER_EXECUTION_TIMELOCK
seconds greater than the timestamp of the block in which the proposal was approved by the Tiebreaker committee.Triggers a transition of the current governance state (if one is possible) before checking the preconditions.
Cancels all currently submitted and non-executed proposals. If a proposal was submitted but not scheduled, it becomes unschedulable. If a proposal was scheduled, it becomes unexecutable.
Triggers a transition of the current governance state, if one is possible.
Registers the proposer
address in the system as a valid proposer and associates it with the executor
contract address (which is expected to be an instance of Executor.sol
) as an executor.
Config.sol
).proposer
address MUST NOT be already registered in the system.executor
instance SHOULD be owned by the EmergencyProtectedTimelock
singleton instance.Removes the registered proposer
address from the list of valid proposers and dissociates it with the executor contract address.
proposer
address MUST be registered in the system as proposer.Updates the address of the Tiebreaker committee.
Triggers a transition of the global governance state, if one is possible; does nothing otherwise.
Issues calls resulting from governance proposals' execution. Every protocol permission or role protected by the DG, as well as the permission to manage this role/permission, should be assigned exclusively to the instances of this contract.
The system supports multiple instances of this contract, but all instances SHOULD be owned by the EmergencyProtectedTimelock
singleton instance.
Issues a EVM call to the target
address with the payload
calldata, optionally sending value
wei ETH.
Reverts if the call was unsuccessful.
The result of the call.
EmergencyProtectedTimelock
singleton instance).The Escrow
contract serves as an accumulator of users' (w)stETH, withdrawal NFTs, and ETH. It has two internal states and serves a different purpose depending on its state:
SignallingEscrow
state. In this state, the contract serves as an oracle for users' opposition to DAO proposals. It allows users to lock and unlock (unlocking is permitted only for the caller after the SignallingEscrowMinLockTime
duration has passed since their last funds locking operation) stETH, wstETH, and withdrawal NFTs, potentially changing the global governance state. The SignallingEscrowMinLockTime
duration, measured in hours, safeguards against manipulating the dual governance state through instant lock/unlock actions within the Escrow
contract instance.RageQuitEscrow
state. In this state, the contract serves as an immutable and ungoverned accumulator for the ETH withdrawn as a result of the rage quit and enforces a timelock on reclaiming this ETH by users.The DualGovernance
contract tracks the current signalling escrow contract using the DualGovernance.signallingEscrow
pointer. Upon the initial deployment of the system, an instance of Escrow
is deployed in the SignallingEscrow
state by the DualGovernance
contract and the DualGovernance.signallingEscrow
pointer is set to this contract.
Each time the governance enters the global RageQuit
state, two things happen simultaneously:
Escrow
instance currently stored in the DualGovernance.signallingEscrow
pointer changes its state from SignallingEscrow
to RageQuitEscrow
. This is the only possible (and thus irreversible) state transition.DualGovernance
contract deploys a new instance of Escrow
in the SignallingEscrow
state and resets the DualGovernance.signallingEscrow
pointer to this newly-deployed contract.At any point in time, there can be only one instance of the contract in the SignallingEscrow
state (so the contract in this state is a singleton) but multiple instances of the contract in the RageQuitEscrow
state.
After the Escrow
instance transitions into the RageQuitEscrow
state, all locked stETH and wstETH tokens are meant to be converted into withdrawal NFTs using the permissionless Escrow.requestNextWithdrawalsBatch()
function.
Once all funds locked in the Escrow
instance are converted into withdrawal NFTs, finalized, and claimed, the main rage quit phase concludes, and the RageQuitExtensionDelay
period begins.
The purpose of the RageQuitExtensionDelay
phase is to provide sufficient time to participants who locked withdrawal NFTs to claim them before Lido DAO's proposal execution is unblocked. As soon as a withdrawal NFT is claimed, the user's ETH is no longer affected by any code controlled by the DAO.
When the RageQuitExtensionDelay
period elapses, the DualGovernance.activateNextState()
function exits the RageQuit
state and initiates the RageQuitEthClaimTimelock
. Throughout this timelock, tokens remain locked within the Escrow
instance and are inaccessible for withdrawal. Once the timelock expires, participants in the rage quit process can retrieve their ETH by withdrawing it from the Escrow
instance.
The duration of the RageQuitEthClaimTimelock
is dynamic and varies based on the number of "continuous" rage quits. A pair of rage quits is considered continuous when DualGovernance
has not transitioned to the Normal
or VetoCooldown
state between them.
Transfers the specified amount
of stETH from the caller's (i.e., msg.sender
) account into the SignallingEscrow
instance of the Escrow
contract.
The total rage quit support is updated proportionally to the number of shares corresponding to the locked stETH (see the Escrow.getRageQuitSupport()
function for the details). For the correct rage quit support calculation, the function updates the number of locked stETH shares in the protocol as follows:
The rage quit support will be dynamically updated to reflect changes in the stETH balance due to protocol rewards or validators slashing.
Finally, calls the DualGovernance.activateNextState()
function. This action may transit the Escrow
instance from the SignallingEscrow
state into the RageQuitEscrow
state.
Escrow
instance MUST be in the SignallingEscrow
state.Escrow
instance equal to or greater than the locked amount
.amount
MUST NOT exceed the caller's stETH balance.Allows the caller (i.e. msg.sender
) to unlock the previously locked stETH in the SignallingEscrow
instance of the Escrow
contract. The locked stETH balance may change due to protocol rewards or validators slashing, potentially altering the original locked amount. The total unlocked stETH equals the sum of all previously locked stETH by the caller, accounting for any changes during the locking period.
For the correct rage quit support calculation, the function updates the number of locked stETH shares in the protocol as follows:
Additionally, the function triggers the DualGovernance.activateNextState()
function at the beginning and end of the execution.
Escrow
instance MUST be in the SignallingEscrow
state.Escrow
instance using the Escrow.lockStETH
function.SignallingEscrowMinLockTime
MUST have passed since the caller last invoked any of the methods Escrow.lockStETH
, Escrow.lockWstETH
, or Escrow.lockUnstETH
.Transfers the specified amount
of wstETH from the caller's (i.e., msg.sender
) account into the SignallingEscrow
instance of the Escrow
contract.
The total rage quit support is updated proportionally to the amount
of locked wstETH (see the Escrow.getRageQuitSupport()
function for the details). For the correct rage quit support calculation, the function updates the number of locked wstETH in the protocol as follows:
Finally, calls the DualGovernance.activateNextState()
function. This action may transit the Escrow
instance from the SignallingEscrow
state into the RageQuitEscrow
state.
Escrow
instance MUST be in the SignallingEscrow
state.Escrow
instance equal to or greater than the locked amount
.amount
MUST NOT exceed the caller's wstETH balance.Allows the caller (i.e. msg.sender
) to unlock previously locked wstETH from the SignallingEscrow
instance of the Escrow
contract. The total unlocked wstETH equals the sum of all previously locked wstETH by the caller.
For the correct rage quit support calculation, the function updates the number of locked wstETH shares in the protocol as follows:
Additionally, the function triggers the DualGovernance.activateNextState()
function at the beginning and end of the execution.
Escrow
instance MUST be in the SignallingEscrow
state.Escrow
instance using the Escrow.lockWstETH
function.SignallingEscrowMinLockTime
MUST have passed since the caller last invoked any of the methods Escrow.lockStETH
, Escrow.lockWstETH
, or Escrow.lockUnstETH
.Transfers the WIthdrawal NFTs with ids contained in the unstETHIds
from the caller's (i.e. msg.sender
) account into the SignallingEscrow
instance of the Escrow
contract.
To correctly calculate the rage quit support (see the Escrow.getRageQuitSupport()
function for the details), updates the number of locked Withdrawal NFT shares in the protocol for each withdrawal NFT in the unstETHIds
, as follows:
Finally, calls the DualGovernance.activateNextState()
function. This action may transition the Escrow
instance from the SignallingEscrow
state into the RageQuitEscrow
state.
Escrow
instance MUST be in the SignallingEscrow
state.SignallingEscrow
instance to transfer tokens with the given ids (approve()
or setApprovalForAll()
).Allows the caller (i.e. msg.sender
) to unlock a set of previously locked Withdrawal NFTs with ids unstETHIds
from the SignallingEscrow
instance of the Escrow
contract.
To correctly calculate the rage quit support (see the Escrow.getRageQuitSupport()
function for details), updates the number of locked Withdrawal NFT shares in the protocol for each withdrawal NFT in the unstETHIds
, as follows:
Escrow.markUnstETHFinalized()
function for details):Additionally, the function triggers the DualGovernance.activateNextState()
function at the beginning and end of the execution.
Escrow
instance MUST be in the SignallingEscrow
state.SignallingEscrowMinLockTime
MUST have passed since the caller last invoked any of the methods Escrow.lockStETH
, Escrow.lockWstETH
, or Escrow.lockUnstETH
.Marks the provided Withdrawal NFTs with ids unstETHIds
as finalized to accurately calculate their rage quit support.
The finalization of the Withdrawal NFT leads to the following events:
As both of these events affect the rage quit support value, this function updates the number of finalized Withdrawal NFTs for the correct rage quit support accounting.
For each Withdrawal NFT in the unstETHIds
:
Withdrawal NFTs belonging to any of the following categories are excluded from the rage quit support update:
Escrow
instanceEscrow
instance MUST be in the SignallingEscrow
state.Calculates and returns the total rage quit support as a percentage of the stETH total supply locked in the instance of the Escrow
contract. It considers contributions from stETH, wstETH, and non-finalized Withdrawal NFTs while adjusting for the impact of locked finalized Withdrawal NFTs.
The returned value represents the total rage quit support expressed as a percentage with a precision of 16 decimals. It is computed using the following formula:
Transits the Escrow
instance from the SignallingEscrow
state to the RageQuitEscrow
state. Following this transition, locked funds become unwithdrawable and are accessible to users only as plain ETH after the completion of the full RageQuit
process, including the RageQuitExtensionDelay
and RageQuitEthClaimTimelock
stages.
As the initial step of transitioning to the RageQuitEscrow
state, all locked wstETH is converted into stETH, and the maximum stETH allowance is granted to the WithdrawalQueue
contract for the upcoming creation of Withdrawal NFTs.
DualGovernance
contract.Escrow
instance MUST be in the SignallingEscrow
state.Transfers stETH held in the RageQuitEscrow
instance into the WithdrawalQueue
. The function may be invoked multiple times until all stETH is converted into Withdrawal NFTs. For each Withdrawal NFT, the owner is set to Escrow
contract instance. Each call creates up to maxWithdrawalRequestsCount
withdrawal requests, where each withdrawal request size equals WithdrawalQueue.MAX_STETH_WITHDRAWAL_AMOUNT()
, except for potentially the last batch, which may have a smaller size.
Upon execution, the function updates the count of withdrawal requests generated by all invocations. When the remaining stETH balance on the contract falls below WITHDRAWAL_QUEUE.MIN_STETH_WITHDRAWAL_AMOUNT()
, the generation of withdrawal batches is concluded, and subsequent function calls will revert.
Escrow
instance MUST be in the RageQuitEscrow
state.maxWithdrawalRequestsCount
MUST be greater than 0Allows users to claim finalized Withdrawal NFTs generated by the Escrow.requestNextWithdrawalsBatch()
function.
Tracks the total amount of claimed ETH updating the _totalClaimedEthAmount
variable. Upon claiming the last batch, the RageQuitExtensionDelay
period commences.
Escrow
instance MUST be in the RageQuitEscrow
state.withdrawalRequestIds
array MUST contain only the ids of finalized but unclaimed withdrawal requests generated by the Escrow.requestNextWithdrawalsBatch()
function.Allows users to claim the ETH associated with finalized Withdrawal NFTs with ids unstETHIds
locked in the Escrow
contract. Upon calling this function, the claimed ETH is transferred to the Escrow
contract instance.
To safeguard the ETH associated with Withdrawal NFTs, this function should be invoked when the Escrow
is in the RageQuitEscrow
state and before the RageQuitExtensionDelay
period ends. The ETH corresponding to unclaimed Withdrawal NFTs after this period ends would still be controlled by the code potentially afftected by pending and future DAO decisions.
Escrow
instance MUST be in the RageQuitEscrow
state.unstETHIds
MUST only contain finalized but unclaimed withdrawal requests with the owner set to msg.sender
.Returns whether the rage quit process has been finalized. The rage quit process is considered finalized when all the following conditions are met:
Escrow
instance is in the RageQuitEscrow
state.RageQuitExtensionDelay
has elapsed.Allows the caller (i.e. msg.sender
) to withdraw all stETH they have previouusly locked into Escrow
contract instance (while it was in the SignallingEscrow
state) as plain ETH, given that the RageQuit
process is completed and that the RageQuitEthClaimTimelock
has elapsed. Upon execution, the function transfers ETH to the caller's account and marks the corresponding stETH as withdrawn for the caller.
The amount of ETH sent to the caller is determined by the proportion of the user's stETH shares compared to the total amount of locked stETH and wstETH shares in the Escrow instance, calculated as follows:
Escrow
instance MUST be in the RageQuitEscrow
state.RageQuitExtensionDelay
duration.RageQuitEthClaimTimelock
period MUST be elapsed after the expiration of the RageQuitExtensionDelay
duration.Allows the caller (i.e. msg.sender
) to withdraw all wstETH they have previouusly locked into Escrow
contract instance (while it was in the SignallingEscrow
state) as plain ETH, given that the RageQuit
process is completed and that the RageQuitEthClaimTimelock
has elapsed. Upon execution, the function transfers ETH to the caller's account and marks the corresponding wstETH as withdrawn for the caller.
The amount of ETH sent to the caller is determined by the proportion of the user's wstETH funds compared to the total amount of locked stETH and wstETH shares in the Escrow instance, calculated as follows:
Escrow
instance MUST be in the RageQuitEscrow
state.RageQuitExtensionDelay
duration.RageQuitEthClaimTimelock
period MUST be elapsed after the expiration of the RageQuitExtensionDelay
duration.Allows the caller (i.e. msg.sender
) to withdraw the claimed ETH from the Withdrawal NFTs with ids unstETHIds
locked by the caller in the Escrow
contract while the latter was in the SignallingEscrow
state. Upon execution, all ETH previously claimed from the NFTs is transferred to the caller's account, and the NFTs are marked as withdrawn.
Escrow
instance MUST be in the RageQuitEscrow
state.RageQuitExtensionDelay
duration.RageQuitEthClaimTimelock
period MUST be elapsed after the expiration of the RageQuitExtensionDelay
duration.Escrow.claimUnstETH()
function.EmergencyProtectedTimelock
is the singleton instance storing proposals approved by DAO voting systems and submitted to the Dual Governance. It allows for setting up time-bound Emergency Activation Committee and Emergency Execution Committee, acting as safeguards for the case of zero-day vulnerability in Dual Governance contracts.
For a proposal to be executed, the following steps have to be performed in order:
EmergencyProtectedTimelock.submit
function.EmergencyProtectedTimelock.schedule
function.EmergencyProtectedTimelock.execute
function.The contract only allows proposal submission and scheduling by the governance
address. Normally, this address points to the DualGovernance
singleton instance. Proposal execution is permissionless, unless Emergency Mode is activated.
If the Emergency Committees are set up and active, the governance proposal gets a separate emergency protection timelock between submitting and scheduling. This additional timelock is implemented in the EmergencyProtectedTimelock
contract to protect from zero-day vulnerability in the logic of DualGovenance.sol
and other core DG contracts. If the Emergency Committees aren't set, the proposal flow is the same, but the timelock duration is zero.
Emergency Activation Committee, while active, can enable the Emergency Mode. This mode prohibits anyone but the Emergency Execution Committee from executing proposals. It also allows the Emergency Execution Committee to reset the governance, effectively disabling the Dual Governance subsystem.
The governance reset entails the following steps:
EmergencyProtectedTimelock
.governance
address to a pre-configured Emergency Governance address. In the simplest scenario, this would be the Lido DAO Aragon Voting contract.Registers a new governance proposal composed of one or more EVM calls
to be made by the executor
contract.
The ID of the successfully registered proposal.
governance
address.governance
address.Instructs the executor contract associated with the proposal to issue the proposal's calls.
Cancels all non-executed proposal, making them forever non-executable.
governance
address.Activates the Emergency Mode.
Executes the scheduled proposal, bypassing the post-schedule delay.
Deactivates the Emergency Activation and Emergency Execution Committees (setting their addresses to 0x00
), cancels all unexecuted proposals, and disables the Protected deployment mode.
emergency mode max duration
ago, MUST be called by the Admin Executor address.Resets the governance
address to the EMERGENCY_GOVERNANCE
value defined in the configuration, cancels all unexecuted proposals, and disables the Protected deployment mode.
The contract has the interface for managing the configuration related to emergency protection (setEmergencyProtection
) and general system wiring (transferExecutorOwnership
, setGovernance
). These functions MUST be called by the Admin Executor address, basically routing any such changes through the Dual Governance mechanics.
In the Lido protocol, specific critical components (WithdrawalQueue
and ValidatorsExitBus
) are safeguarded by the GateSeal
contract instance. According to the gate seals documentation:
"A GateSeal is a contract that allows the designated account to instantly put a set of contracts on pause (i.e. seal) for a limited duration. This will give the Lido DAO the time to come up with a solution, hold a vote, implement changes, etc.".
However, the effectiveness of this approach is contingent upon the predictability of the DAO's solution adoption timeframe. With the dual governance system, proposal execution may experience significant delays based on the current state of the DualGovernance
contract. There's a risk that GateSeal
's pause period may expire before the Lido DAO can implement the necessary fixes.
To address this compatibility challenge between gate seals and dual governance, the GateSealBreaker
contract is introduced. The GateSealBreaker
enables the trustless unpause of contracts sealed by a GateSeal
instance, but only under specific conditions:
GateSeal
contract has elapsed.For seamless integration with the DualGovernance
and GateSealBreaker
contracts, the GateSeal
instance will be configured as follows:
MAX_SEAL_DURATION_SECONDS
and SEAL_DURATION_SECONDS
are set to type(uint256).max
, what equivalent to PAUSE_INFINITELY
, for the PausableUntil.sol contract.MIN_SEAL_DURATION_SECONDS
is set to a finite duration, allowing the Lido DAO sufficient time to respond and adopt proposals when the DualGovernance
contract is in the Normal
state.With such settings, the GateSeal
instance seals the contracts indefinitely. However, anyone can initiate the process of "breaking the seal" by calling the GateSealBreaker.startRelease(address gateSeal)
function, provided both requirements are met:
MIN_SEAL_DURATION_SECONDS
has elapsed since the committee activated the GateSeal
.DualGovernance
is currently in the Normal
or VetoCooldown
state, allowing proposals scheduling.The GateSealBreaker.startRelease()
function can be called only once for each activated GateSeal
contract registered in the GateSealBreaker
. This function effectively begins the countdown to release the seal, starting the RELEASE_DELAY
.
During the RELEASE_DELAY
, the sealed contracts remain paused, providing the Lido DAO time to schedule proposals within the dual governance system (the scheduling is allowed, which is guaranteed by the governance state precondition of the GateSealBreaker.startRelease
function).
Upon completion of the RELEASE_DELAY
, the GateSealBreaker.enactRelease(address gateSeal)
function can be called to unpause the sealed contracts. This function is trustless and may only be called once. It does not revert even if some or all attempts to unpause the sealed contracts fail.
This function should be invoked by the Lido DAO during the setup of the GateSeal
instance. Upon registration in the contract, an activated GateSeal
instance becomes eligible for release using the startRelease()
/enactRelease()
methods.
GateSeal
instance being registered MUST NOT have been previously registered.Initiates the release process for the activated GateSeal
instance registered in the contract. Records the release initiation timestamp and starts the RELEASE_DELAY
period for the specific gateSeal
.
gateSeal
MUST be registered in the contract.gateSeal
MUST be activated by the gate seal committee.MIN_SEAL_DURATION_SECONDS
MUST have passed since the activation of the gateSeal
.gateSeal
MUST NOT be already released.DualGovernance
contract MUST be in either the Normal
or VetoCooldown
state.Unpauses all contracts sealed by the specified gateSeal
once the RELEASE_DELAY
has elapsed since the release initiation.
Retrieves all sealed contracts via the GateSeal.sealed_sealables()
view function and calls IPausableUntil(sealable).resume()
for each sealed contract.
If any call to a sealable, including the resume()
call, fails during the execution, the transaction WILL NOT revert but will emit the ErrorWhileResuming(sealable, lowLevelError)
event for each contract that failed to unpause.
GateSealBreaker.startRelease()
function MUST be called for the specified gateSeal
.RELEASE_DELAY
for the specified gateSeal
MUST have elapsed since the release initiation.GateSealBreaker
contract SHOULD have been granted rights to unpause the sealed contracts.Configuration.sol
is the smart contract encompassing all the constants in the Dual Governance design & providing the interfaces for getting access to them. It implements interfaces IAdminExecutorConfiguration
, ITimelockConfiguration
, IDualGovernanceConfiguration
covering for relevant "parameters domains".
In designing the dual governance system, ensuring seamless updates while maintaining the contracts' immutability was a primary consideration. To achieve this, the system was divided into three key components: DualGovernance
, EmergencyProtectedTimelock
, and Executor
.
When updates are necessary only for the DualGovernance
contract logic, the EmergencyProtectedTimelock
and Executor
components remain unchanged. This simplifies the process, as it only requires deploying a new version of the DualGovernance
. This approach preserves proposal history and avoids the complexities of redeploying executors or transferring rights from previous instances.
During the deployment of a new dual governance version, the Lido DAO will likely launch it under the protection of the emergency committee, similar to the initial launch (see Proposal execution and deployment modes for the details). The EmergencyProtectedTimelock
allows for the reassembly and reactivation of emergency protection at any time, even if the previous committee's duration has not yet concluded.
A typical proposal to update the dual governance system to a new version will likely contain the following steps:
governance
variable in the EmergencyProtectedTimelock
instance to the new version of the DualGovernance
contract.Configuration
proxy contract if necessary.EmergencyProtectedTimelock
contract, including the address of the committee, the duration of emergency protection, and the duration of the emergency mode.For more significant updates involving changes to the EmergencyProtectedTimelock
or Proposals
mechanics, new versions of both the DualGovernance
and EmergencyProtectedTimelock
contracts are deployed. While this adds more steps to maintain the proposal history, such as tracking old and new versions of the Timelocks, it also eliminates the need to migrate permissions or rights from executors. The transferExecutorOwnership()
function of the EmergencyProtectedTimelock
facilitates the assignment of executors to the newly deployed contract.