From eaff08787a605381415dead841fbdca6fca5dcbb Mon Sep 17 00:00:00 2001 From: Sam Kozin Date: Thu, 4 Apr 2024 15:35:57 +0300 Subject: [PATCH] docs: add the WIP version of the spec --- docs/mechanism.md | 329 ++++++++++++++ docs/specification.md | 999 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1328 insertions(+) create mode 100644 docs/mechanism.md create mode 100644 docs/specification.md diff --git a/docs/mechanism.md b/docs/mechanism.md new file mode 100644 index 00000000..9de600ff --- /dev/null +++ b/docs/mechanism.md @@ -0,0 +1,329 @@ +**Working draft**, the latest version is published at https://hackmd.io/@skozin/rkD1eUzja. + +--- + +# Dual Governance mechanism design + +A proposal by [sam](https://twitter.com/_skozin), [pshe](https://twitter.com/PsheEth), [kadmil](https://twitter.com/kadmil_eth), [sacha](https://twitter.com/sachayve), [psirex](https://twitter.com/psirex_), [Hasu](https://twitter.com/hasufl), [Izzy](https://twitter.com/IsdrsP), and [Vasiliy](https://twitter.com/_vshapovalov). + +Currently, the Lido protocol governance consists of the Lido DAO that uses LDO voting to approve DAO proposals, along with an optimistic voting subsystem called Easy Tracks that is used for routine changes of low-impact parameters and falls back to LDO voting given any objection from LDO holders. + +Additionally, there is a Gate Seal emergency committee that allows pausing certain protocol functionality (e.g. withdrawals) for a pre-configured amount of time sufficient for the DAO to vote on and execute a proposal. Gate Seal committee can only enact a pause once before losing its power (so it has to be re-elected by the DAO after that). + +The Dual governance mechanism (DG) is an iteration on the protocol governance that gives stakers a say by allowing them to block DAO decisions and providing a negotiation device between stakers and the DAO. + +Another way of looking at dual governance is that it implements 1) a dynamic user-extensible timelock on DAO decisions and 2) a rage quit mechanism for stakers taking into account the specifics of how Ethereum withdrawals work. + +## Definitions + +* **Lido protocol:** code deployed on the Ethereum blockchain implementing: + 1. a middleware between the parties willing to delegate ETH for validating the Ethereum blockchain in exchange for staking rewards (stakers) and the parties willing to run Ethereum validators in exchange for a fee taken from staking rewards (node operators); + 2. a fungibility layer distributing ETH between node operators and issuing stakers a fungible deposit receipt token (stETH). +* **Protocol governance:** the mechanism allowing to change the Lido protocol parameters and upgrade non-ossified (mutable) parts of the protocol code. +* **LDO:** the fungible governance token of the Lido DAO. +* **Lido DAO:** code deployed on the Ethereum blockchain implementing a DAO that receives a fee taken from the staking rewards to its treasury and allows LDO holders to collectively vote on spending the treasury, changing parameters of the Lido protocol and upgrading the non-ossified parts of the Lido protocol code. Referred to as just **DAO** thoughout this document. +* **DAO proposal:** a specific change in the onchain state of the Lido protocol or the Lido DAO proposed by LDO holders. Proposals have to be approved via onchain voting between LDO holders to become executable. +* **stETH:** the fungible deposit receipt token of the Lido protocol. Allows the holder to withdraw the deposited ETH plus all accrued rewards (minus the fees) and penalties. Rewards/penalties accrual is expressed by periodic rebases of the token balances. +* **wstETH:** a non-rebasable, immutable, and trustless wrapper around stETH deployed as an integral part of the Lido protocol. At any moment in time, there is a fixed wstETH/stETH rate effective for wrapping and unwrapping. The rate changes on each stETH rebase. +* **Withdrawal NFT:** a non-fungible token minted by the Lido withdrawal queue contract as part of the (w)stETH withdrawal to ETH, parametrized by the underlying (w)stETH amount and the position in queue. Gives holder the right to claim the corresponding ETH amount after the withdrawal is complete. Doesn't entitle the holder to receive staking rewards. +* **Stakers:** EOAs and smart contract wallets that hold stETH, wstETH tokens, and withdrawal NFTs or deposit them into various deFi protocols and ceFi platforms: DEXes, CEXes, lending and stablecoin protocols, custodies, etc. +* **Node operators:** parties registered in the Lido protocol willing to run Ethereum validators using the delegated ETH in exchange for a fee taken from the staking rewards. Node operators generate validator keys and at any time remain their sole holders, having full and exclusive control over Ethereum validators. Node operators are required to set their validators' withdrawal credentials to point to the specific Lido protocol smart contract. + +## Mechanism description + +### Proposal lifecycle + +The DG assumes that any permissions [protected by the subsystem](#Dual-governance-scope) (which we will call the in-scope permissions) are assigned to the DG contracts, in contrast to being assigned to the DAO voting systems. Thus, it's impossible for the DAO to execute any in-scope changes bypassing the DG. + +Instead of making the in-scope changes directly, the DAO voting script should submit them as a proposal to the DG subsystem upon execution of the approved DAO vote. + +![image](https://github.com/lidofinance/dual-governance/assets/1699593/5130780d-edb5-4210-ac16-2f76a0dfd5b8) + +After submission to the DG, a proposal can exist in one of the following states: + +* **Pending**: a proposal approved by the DAO was submitted to the DG subsystem, starting the dynamic execution timelock. +* **Cancelled**: the DAO votes for cancelling the pending proposal. This is the terminal state. +* **Executed**: the dynamic timelock of a pending proposal has elapsed and the proposal was executed. This is the terminal state. + + +### Signalling Escrow + +At any point in time, stakers can signal their opposition to the DAO by locking their stETH, wstETH, and [unfinalized](https://docs.lido.fi/contracts/withdrawal-queue-erc721#finalization) withdrawal NFTs into a dedicated smart contract called **veto signalling escrow**. A staker can also lift this signal by unlocking their tokens from the escrow given that at least `SignallingEscrowMinLockTime` passed since this staker locked token(s) the last time. This creates an onchain oracle for measuring stakers' disagreement with the DAO decisions. + +While stETH or wstETH tokens are locked in the signalling escrow, they still generate staking rewards and are still subject to potential slashings. + +An address having stETH or wstETH locked in the signalling escrow can trigger an immediate withdrawal of the locked tokens to ETH while keeping the resulting withdrawal NFT locked in the signalling escrow. + +Let's define the **rage quit support** $R$ as a dimensionless quantity calculated as follows: + +```math +R = \frac{1}{ S_{st} + \text{eth}_f } \left( + \text{st} + \text{eth}_f + + R_{wst}^{st} ( \text{wst} + \text{shares}_u ) +\right) +``` + +```math +\text{eth}_f = \sum_{N_i \in \text{WR}_f} \text{eth}(N_i) +``` + +```math +\text{shares}_u = \sum_{N_i \in \text{WR}_u} \text{shares}(N_i) +``` + +where + +* $S_{st}$ is the current stETH total supply, +* $\text{st}$ is the total amount of stETH locked in the signalling escrow, +* $R_{wst}^{st}$ is the current conversion rate from wstETH to stETH (the result of the `stETH.getPooledEthByShares(10**18) / 10**18` call), +* $\text{wst}$ is the total amount of wstETH locked in the signalling escrow, +* $\text{WR}_f$ is the set of finalized withdrawal NFTs locked in the signalling escrow, +* $\text{WR}_u$ is the set of non-finalized withdrawal NFTs locked in the escrow, +* $\text{shares}(N_i)$ is the stETH shares amount corresponding to the unfinalized withdrawal NFT $N_i$, +* $\text{eth}(N_i)$ is the withdrawn ETH amount associated with the finalized withdrawal NFT $N_i$. + +Changes in the rage quit support act as the main driver for the global governance state transitions. + +```env +# Proposed values, to be modeled and refined +SignallingEscrowMinLockTime = 5 hours +``` + + +### Global governance state + +The DG mechanism can be described as a state machine defining the global governance state, with each particular state imposing different limitations on the actions the DAO can perform, and state transitions being driven by stakers' actions and (w)stETH withdrawals processing. + +|State |DAO can submit proposals|DAO can execute proposals| +|-------------------------------|---|---| +|Normal | ✓ | ✓ | +|Veto Signalling | ✓ | | +|Veto Signalling (deactivation) | | | +|Veto Cooldown | | ✓ | +|Rage Quit | ✓ | | + +![image](https://github.com/lidofinance/dual-governance/assets/1699593/862b3f11-ea79-4e75-8c56-ff56f94d0a6f) + +Let's now define these states and transitions. + + +### Normal state + +The Normal state is the state the mechanism is designed to spend the most time within. The DAO can submit the approved proposals to the DG and execute them after the standard timelock of `ProposalExecutionMinTimelock` days. + +If, while the state is active, the [rage quit support](#Signalling-Escrow) exceeds `FirstSealRageQuitSupport`, and at least `NormalStateMinDuration` seconds passed since the moment of the state activation, the governance is transferred into the Veto Signalling state. + +```env +# Proposed values, to be modeled and refined +ProposalExecutionMinTimelock = 3 days +FirstSealRageQuitSupport = 0.01 +NormalStateMinDuration = 5 hours +``` + + +### Veto Signalling state + +The Veto Signalling state's purpose is two-fold: + +1. Reduce information asymmetry by allowing an active minority of stakers to block the execution of a controversial DAO decision until it can be inspected and acted upon by the less active majority of stakers. +2. Provide a negotiation vehicle between stakers and the DAO. + +In this state, the DAO can submit approved proposals to the DG but cannot execute them, including the proposals that were pending prior to the governance entering this state, effectively extending the timelock on all such proposals. + +The only proposal that can be executed by the DAO is the special $CancelAllPendingProposals$ action that cancels all proposals that were pending at the moment of this execution, making them forever unexecutable. This mechanism provides a way for the DAO and stakers to negotiate and de-escalate if consensus is reached. + +The **current duration** $T^S(t)$ of the Veto Signalling state is the time passed since the activation of the state. The time spent in the [Deactivation](#Deactivation-sub-state) sub-state is counted towards the current Veto Signalling duration (since the parent state remains active). + +The **target duration** $T^S_{target}(R)$ of the state depends on the current rage quit support $R$ and can be calculated as follows: + +```math +T^S_{target}(R) = +\left\{ \begin{array}{lr} + 0, & \text{if } R \lt R_1 \\ + L(R), & \text{if } R_1 \leq R \leq R_2 \\ + T^S_{max}, & \text{if } R \gt R_2 +\end{array} \right. +``` + +```math +L(R) = T^S_{min} + \frac{(R - R_1)} {R_2 - R_1} (T^S_{max} - T^S_{min}) +``` + +where $R_1$ is `FirstSealRageQuitSupport`, $R_2$ is `SecondSealRageQuitSupport`, $T^S_{min}$ is `VetoSignallingMinDuration`, $T^S_{max}$ is `VetoSignallingMaxDuration`. The dependence of the target duration on the rage quit support can be illustrated by the following graph: + +![image](https://github.com/lidofinance/dual-governance/assets/1699593/e118e856-7cb1-438a-9bda-bcb6b3960cf0) + +When the current rage quit support changes due to stakers locking or unlocking tokens into/out of the signalling escrow or the total stETH supply changing, the target duration is re-evaluated. + +The **time since re-activation** $T^{Sr}(t)$ is the time passed since the Deactivation sub-state was exited the last time, leaving only the parent Veto Signalling state active. + +If, while Veto Signalling is active, the following condition becomes true: + +```math +\left(T^S(t) > T^S_{target}(R)\right) \, \land \, \left(T^{Sr}(t) > T^{Sr}_{min}\right) +``` + +where $T^{Sr}_{min}$ is `VetoSignallingMinReactivationDuration`, then either of the following happens: + +1. if the rage quit support $R$ is below the `SecondSealRageQuitSupport`, the Deactivation sub-state of the Veto Signalling state is entered without exiting the parent Veto Signalling state, +2. otherwise, the Veto Signalling state is exited and the governance is transferred to the Rage Quit state (this can happen iff the governance has already spent `VetoSignallingMaxDuration` in this state). + +```env +# Proposed values, to be modeled and refined +VetoSignallingMinDuration = 5 days +VetoSignallingMaxDuration = 45 days +VetoSignallingMinReactivationDuration = 5 hours +SecondSealRageQuitSupport = 0.1 +``` + +#### Deactivation sub-state + +The sub-state's purpose is to allow all stakers to observe the Veto Signalling being deactivated and react accordingly before non-cancelled proposals can be executed. In this sub-state, the DAO cannot submit proposals to the DG or execute pending proposals. + +Since this is a sub-state, the time it's being active counts towards the parent Veto Signalling state duration. + +The maximum time this sub-state can remain active, $T^D_{max}$, is calculated at the moment it gets entered as follows: if there were no proposals submitted to the DG since the last activation of the Veto Signalling state, then + +```math +T^D_{max} = T^D_{min} +``` + +where $T^D_{min}$ is `VetoSignallingDeactivationMinDuration`. Otherwise, it's defined by the following expression: + +```math +T^D_{max} = \max \left\{ T^D_{min}, \; t_{prop} + T^S_{max} - t \right\} +``` + +where $t_{prop}$ is the moment the last proposal was submitted to the DG, $T^S_{max}$ is `VetoSignallingMaxDuration`, $t$ is the current time, i.e. the moment the Deactivation state is entered at. + +If the current duration of the Deactivation state becomes larger than $T^D_{max}$, the Deactivation sub-state is exited along with its parent Veto Signalling state and the governance is transferred to the Veto Cooldown state. + +If, while the sub-state is active and as the result of the rage quit support changing, the target duration of the Veto Signalling state becomes more than its current duration (which includes the time the Deactivation sub-state is being active), the Deactivation sub-state is exited so only the main Veto Signalling state remains active. + +If, while the sub-state is active, the rage quit support exceeds the `SecondSealRageQuitSupport` AND the current duration of the Veto Signalling state exceeds the `VetoSignallingMaxDuration`, the Deactivation sub-state and its parent Veto Signalling state are exited and the governance is transferred to the Rage Quit state. + +```env +# Proposed values, to be modeled and refined +VetoSignallingDeactivationMinDuration = 3 days +``` + +### Veto Cooldown state + +In the Veto Cooldown state, the DAO cannot submit proposals to the DG but can execute pending non-cancelled proposals. It exists to guarantee that no staker possessing `FirstSealRageQuitSupport` stETH can lock the governance indefinitely without rage quitting the protocol. + +The state duration is fixed at `VetoCooldownDuration`. After this time passes since the state activation, the state is exited and the governance is transferred either to the Normal state (if the rage quit support at that moment is less than `FirstSealRageQuitSupport`) or to the Veto Signalling state (otherwise). + +```env +# Proposed values, to be modeled and refined +VetoCooldownDuration = 5 hours +``` + + +### Rage Quit state + +The Rage Quit state allows all stakers who elected to leave the protocol via rage quit to fully withdraw their ETH without being subject to any new or pending DAO decisions. Entering this state means that stakers and the DAO weren't able to resolve the dispute so the DAO is misaligned with a significant part of stakers. + +Upon entry into the Rage Quit state, three things happen: + +1. The veto signalling escrow is irreversibly transformed into the **rage quit escrow**, an immutable smart contract that holds all tokens that are part of the rage quit withdrawal process, i.e. stETH, wstETH, withdrawal NFTs, and the withdrawn ETH, and allows stakers to claim the withdrawn ETH after a certain timelock following the completion of the withdrawal process (with the timelock being determined at the moment of the Rage Quit state entry). +2. All stETH and wstETH held by the rage quit escrow are sent for withdrawal via the regular Lido Withdrawal Queue mechanism, generating a set of batch withdrawal NFTs held by the rage quit escrow. +3. A new instance of the veto signalling escrow smart contract is deployed. This way, at any point in time, there is only one veto signalling escrow but there may be multiple rage quit escrows from previous rage quits. + +In this state, the DAO is allowed submit proposals to the DG but cannot execute any pending proposals. Stakers are not allowed to lock (w)stETH or withdrawal NFTs into the rage quit escrow so joining the ongoing rage quit is not possible. However, they can lock their tokens that are not part of the ongoing rage quit process to the newly-deployed veto signalling escrow to potentially trigger a new rage quit later. + +The state lasts until the withdrawal started in 2) is complete, i.e. until all batch withdrawal NFTs generated from (w)stETH that was locked in the escrow are fulfilled and claimed, plus `RageQuitExtensionDelay` days. + +If, prior to the Rage Quit state being entered, a staker locked a withdrawal NFT into the signalling escrow, this NFT remains locked in the rage quit escrow. When such an NFT becomes fulfilled, the staker is allowed to burn this NFT and convert it to plain ETH, although still locked in the escrow. This allows stakers to derisk their ETH as early as possible by removing any dependence on the DAO decisions (remember that the withdrawal NFT contract is potentially upgradeable by the DAO but the rage quit escrow is immutable). + +Since batch withdrawal NFTs are generated after the NFTs that were locked by stakers into the escrow directly, the withdrawal queue mechanism guarantees that, by the time batch NFTs are fulfilled, all individually locked NFTs are fulfilled as well and can be claimed. Together with the extension delay, this guarantees that any staker having a withdrawal NFT locked in the rage quit escrow has at least `RageQuitExtensionDelay` days to convert it to escrow-locked ETH before the DAO execution is unblocked. + +When the withdrawal is complete and the extension delay elapses, two things happen simultaneously: + +1. A timelock lasting $W(i)$ days is started, during which the withdrawn ETH remains locked in the rage quit escrow. After the timelock elapses, stakers who participated in the rage quit can obtain their ETH from the rage quit escrow. +2. The governance exits the Rage Quit state. + +The next state depends on the current rage quit support (which depends on the amount of tokens locked in the veto signalling escrow deployed in 3): if it exceeds `FirstSealRageQuitSupport`, the governance is transferred to the Veto Signalling state; otherwise, to the Veto Cooldown state. + +The duration of the ETH claim timelock $W(i)$ is a non-linear function that depends on the rage quit sequence number $i$ (see below): + +```math +W(i) = W_{min} + +\left\{ \begin{array}{lc} + 0, & \text{if } i \lt i_{min} \\ + g_W(i - i_{min}), & \text{otherwise} +\end{array} \right. +``` + +where $W_{min}$ is `RageQuitEthClaimMinTimelock`, $i_{min}$ is `RageQuitEthClaimTimelockGrowthStartSeqNumber`, and $g_W(x)$ is a quadratic polynomial function with coefficients `RageQuitEthClaimTimelockGrowthCoeffs` (a list of length 3). + +The rage quit sequence number is calculated as follows: each time the Normal state is entered, the sequence number is set to 0; each time the Rage Quit state is entered, the number is incremented by 1. + +```env +# Proposed values, to be modeled and refined +RageQuitExtensionDelay = 7 days +RageQuitEthClaimMinTimelock = 60 days +RageQuitEthClaimTimelockGrowthStartSeqNumber = 2 +RageQuitEthClaimTimelockGrowthCoeffs = (0, TODO, TODO) +``` + +### Gate Seal behavior and Tiebreaker Committee + +The [Gate Seal](https://docs.lido.fi/contracts/gate-seal) is an existing circuit breaker mechanism designed to be activated in the event of a zero-day vulnerability in the protocol contracts being found or exploited and empowering a DAO-elected committee to pause certain protocol functionality, including withdrawals, for a predefined duration enough for the DAO to vote for and execute a remediation. When this happens, the committee immediately loses its power. If this never happens, the committee's power also expires after a pre-configured amount of time passes since its election. + +The pre-defined pause duration currently works since all DAO proposals have a fixed execution timelock so it's possible to configure the pause in a way that would ensure the DAO has enough time to vote on a fix, wait until the execution timelock expires, and execute the proposal before the pause ends. + +The DG mechanism introduces a dynamic timelock on DAO proposals dependent on stakers' actions and protocol withdrawals processing which, in turn, requires making the Gate Seal pause duration also dynamic for the Gate Seal to remain an efficient circuit-breaker. + +#### Gate Seal behaviour (updated) + +If, at any moment in time, two predicates become true simultaneously: + +1. any DAO-managed contract functionality is paused by a Gate Seal; +2. the DAO execution is blocked by the DG mechanism (i.e. the global governance state is Veto Signalling, Veto Signalling Deactivation, or Rage Quit), + +then the Gate Seal-induced pause is prolonged until the DAO execution is unblocked by the DG, i.e. until the global governance state becomes Normal or Veto Cooldown. + +Otherwise, the Gate Seal-induced pause lasts for a pre-defined fixed duration. + +#### Tiebreaker Committee + +Given the updated Gate Seal behaviour, the system allows for reaching a deadlock state: if the protocol withdrawals functionality gets paused by the Gate Seal committee while the governance state is Rage Quit, or if it gets paused before and remains paused until the Rage Quit starts, then withdrawals should remain paused until the Rage Quit state is exited and the DAO execution is unblocked. But the Rage Quit state lasts until all staked ETH participating in the rage quit is withdrawn to ETH, which cannot happen while withdrawals are paused. + +Apart from the Gate Seal being activated, withdrawals can become dysfunctional due to a bug in the protocol code. If this happens while the Rage Quit state is active, it would also trigger the deadlock since a DAO proposal fixing the bug cannot be executed until the Rage Quit state is exited. + +To resolve the potential deadlock, the mechanism contains a third-party arbiter **Tiebreaker Committee** elected by the DAO. The committee gains its power only under the specific conditions of the deadlock (see below), and the power is limited by bypassing the DG dynamic timelock for pending proposals approved by the DAO. + +Specifically, the Tiebreaker committee can execute any pending proposal submitted by the DAO to DG, subject to a timelock of `TiebreakerExecutionTimelock` days, iff any of the following two conditions is true: + +* **Tiebreaker Condition A**: (governance state is Rage Quit) AND (protocol withdrawals are paused by a Gate Seal). +* **Tiebreaker Condition B**: (governance state is Rage Quit) AND (last time governance exited Normal or Veto Cooldown state was more than `TiebreakerActivationTimeout` days ago). + +The Tiebreaker committee should be composed of multiple sub-committees covering different interest groups within the Ethereum community (e.g. largest DAOs, EF, L2s, node operators, OGs) and should require approval from a supermajority of sub-committees in order to execute a pending proposal. The approval by each sub-committee should require the majority support within the sub-committee. No sub-committee should contain more than $1/4$ of the members that are also members of the withdrawals Gate Seal committee. + +The composition of the Tiebreaker committee should be set by a DAO vote (subject to DG) and reviewed at least once a year. + +```env +# Proposed values, to be modeled and refined +TiebreakerExecutionTimelock = 1 month +TieBreakerActivationTimeout = 1 year +``` + +## Dual governance scope + +Dual governance should cover any DAO proposal that could potentially affect the protocol users, including: + +* Upgrading, adding, and removing any protocol code. +* Changing the global protocol parameters and safety limits. +* Changing the parameters of: + * The withdrawal queue. + * The staking router, including addition and removal of staking modules. + * Staking modules, including addition and removal of node operators within the curated staking module. +* Adding, removing, and replacing the oracle committee members. +* Adding, removing, and replacing the deposit security committee members. + +Importantly, any change to the parameters of the dual governance contracts (including managing the tiebreaker committee structure) should be also in the scope of dual governance. + +Dual governance should not cover: + +* Emergency actions triggered by circuit breaker committees and contracts, including activation of any Gate Seal. These actions must be limited in scope and time and must be unable to change any protocol code. +* DAO decisions related to spending and managing the DAO treasury. diff --git a/docs/specification.md b/docs/specification.md new file mode 100644 index 00000000..e250f4b7 --- /dev/null +++ b/docs/specification.md @@ -0,0 +1,999 @@ +**Working draft**, the latest version is published at https://hackmd.io/@skozin/SkjuZAuip. + +--- + +# Dual Governance specification + +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 overview][mech design] document which should be considered an integral part of this specification. + +[mech design]: https://hackmd.io/@skozin/rkD1eUzja + +[mech design - tiebreaker]: https://hackmd.io/@skozin/rkD1eUzja#Tiebreaker-Committee + + +## System overview + +![image](https://github.com/lidofinance/dual-governance/assets/1699593/8b1f119c-2a61-4d66-969c-acab2b66c16e) + +The system is composed of the following main contracts: + +* [`DualGovernance.sol`](#Contract-DualGovernancesol) 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`](#Contract-EmergencyProtectedTimelocksol) 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-Executorsol) 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`](#Contract-Escrowsol) 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](#Rage-quit). +* [`GateSealBreaker.sol`](#contract-gatesealbreakersol) is a singleton that allows anyone to unpause the protocol contracts that were put into an emergency pause by the [GateSeal emergency protection mechanism](https://github.com/lidofinance/gate-seals), given that the minimum pause duration has passed and that the DAO execution is not currently blocked by the DG system. + + +## Proposal flow + +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. + +![image](https://github.com/lidofinance/dual-governance/assets/1699593/dc4b2a7c-8092-4195-bd68-f5581850fc6c) + +The general proposal flow is the following: + +1. A proposer submits a proposal, i.e. a set of EVM calls (represented by an array of [`ExecutorCall`](#Struct-ExecutorCall) structs) to be issued by the proposer's associated [executor contract](#Contract-Executorsol), by calling the [`DualGovernance.submitProposal`](#Function-DualGovernancesubmitProposal) function. +2. This starts a [dynamic timelock period](#Dynamic-timelock) that allows stakers to oppose the DAO, potentially leaving the protocol before the timelock elapses. +3. By the end of the dynamic timelock period, the proposal is either canceled by the DAO or executable. + * If it's canceled, it cannot be scheduled for execution. However, any proposer is free to submit a new proposal with the same set of calls. + * Otherwise, anyone can schedule the proposal for execution by calling the [`DualGovernance.scheduleProposal`](#Function-DualGovernancescheduleProposal) function, with the execution flow that follows being dependent on the [deployment mode](#Proposal-execution-and-deployment-modes). +4. The proposal's execution results in the proposal's EVM calls being issued by the executor contract associated with the proposer. + + +### Dynamic timelock + +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](#Contract-Escrowsol). If the opposition exceeds some minimum threshold, the [global governance state](#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. + +![image](https://github.com/lidofinance/dual-governance/assets/1699593/98273df0-f3fd-4149-929d-3315a8e81aa8) + +At any time, the DAO can cancel all pending proposals by calling the [`DualGovernance.cancelAllPendingProposals`](#Function-DualGovernancecancelAllPendingProposals) function. + +By the time the dynamic timelock described above elapses, one of the following outcomes is possible: + +* The DAO was not opposed by stakers (the **happy path** scenario). +* The DAO was opposed by stakers and canceled all pending proposals (the **two-sided de-escalation** scenario). +* The DAO was opposed by stakers and didn't cancel pending proposals, forcing the stakers to leave via the rage quit process, or canceled the proposals but some stakers still left (the **rage quit** scenario). +* The DAO was opposed by stakers and didn't cancel pending proposals but the total stake opposing the DAO was too small to trigger the rage quit (the **failed escalation** scenario). + + +### Proposal execution and deployment modes + +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. + +![image](https://github.com/lidofinance/dual-governance/assets/1699593/6252bc65-269f-447d-b215-6a59188b8a94) + +#### Regular deployment mode + +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-EmergencyProtectedTimelockexecute) function. + +#### Protected deployment mode + +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`](#Funtion-EmergencyProtectedTimelockexecute), one has to wait until an **emergency protection timelock** elapses since the proposal scheduling time. + +![image](https://github.com/lidofinance/dual-governance/assets/1699593/38cb2371-bdb0-4681-9dfd-356fa1ed7959) + +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. + + +## Governance state + +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][mech 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. + +![image](https://github.com/lidofinance/dual-governance/assets/1699593/44c2b253-6ea2-4aac-a1c6-fd54cec92887) + +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][mech design] for more details): + +1. **Rage quit support** changing due to stakers locking and unlocking their tokens into/out of the veto signalling escrow or stETH total supply changing; +2. Protocol withdrawals processing (in the `RageQuit` state); +3. Time passing. + +![image](https://github.com/lidofinance/dual-governance/assets/1699593/570141e8-4d66-45a2-9d18-3435806f1831) + + +## Rage quit + +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. + +![image](https://github.com/lidofinance/dual-governance/assets/1699593/4b42490e-4d67-4277-b1e1-390d4c385ca8) + +In the [governance state machine](#Governance-state), 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: + +![image](https://github.com/lidofinance/dual-governance/assets/1699593/419f5621-7f83-4360-8f81-d3ced27b9fcc) + + +## Tiebreaker committee + +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][mech design - tiebreaker] 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-DualGovernancesetTiebreakerCommittee) function. + +While the deadlock conditions are met, the tiebreaker committee address is allowed to approve execution of any pending proposal by calling [`DualGovernance.tiebreakerApproveProposal`](#Function-DualGovernancetiebreakerApproveProposal) so that its execution can be scheduled after the tiebreaker execution timelock passes by calling [`DualGovernance.tiebreakerScheduleProposal`](#Function-DualGovernancetiebreakerScheduleProposal). + + +## Administrative actions + +The dual governance system supports a set of administrative actions, including: + +* Changing the configuration options. +* [Upgrading the system's code](#Upgrade-flow-description). +* Managing the [deployment mode](#Proposal-execution-and-deployment-modes): configuring or disabling the emergency protection delay, setting the emergency committee addresses and lifetime. +* Setting the [Tiebreaker committee](#Tiebreaker-committee) address. + +Each of these actions can only be performed by a designated **admin executor** contract (set by a configuration option), meaning that: + +1. It has to be proposed by one of the proposers associated with this executor. Such proposers are called **admin proposers**. +2. It has to go through the dual governance execution flow with stakers having the power to object. + + +## Common types + +### Struct: ExecutorCall + +```solidity +struct ExecutorCall { + address target; + uint96 value; + bytes payload; +} +``` + +Encodes an EVM call from an executor contract to the `target` address with the specified `value` and the calldata being set to `payload`. + + +## Contract: DualGovernance.sol + +The main entry point to the dual governance system. + +* Provides an interface for submitting and cancelling governance proposals and implements a dynamic timelock on scheduling their execution. +* Manages the list of supported proposers (DAO voting systems). +* Implements a state machine tracking the current [global governance state](#Governance-state) which, in turn, determines whether proposal submission and execution is currently allowed. +* Deploys and tracks the [`Escrow`](#Contract-Escrowsol) contract instances. Tracks the current signalling escrow. + +This contract is a singleton, meaning that any DG deployment includes exectly one instance of this contract. + + +### Enum: DualGovernance.State + +```solidity +enum State { + Normal, + VetoSignalling, + VetoSignallingDeactivation, + VetoCooldown, + RageQuit +} +``` + +Encodes the current global [governance state](#Governance-state), affecting the set of actions allowed for each of the system's actors. + + +### Function: DualGovernance.submitProposal + +```solidity +function submitProposal(ExecutorCall[] calls) + returns (uint256 proposalId) +``` + +Instructs the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) 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](#Function-DualGovernancescheduleProposal) for execution. + +See: [`EmergencyProtectedTimelock.submit`](#Function-EmergencyProtectedTimelocksubmit). + +#### Returns + +The id of the successfully registered proposal. + +#### Preconditions + +* The calling address MUST be [registered as a proposer](#Function-DualGovernanceregisterProposer). +* The current governance state MUST be either of: `Normal`, `VetoSignalling`, `RageQuit`. + +Triggers a transition of the current governance state (if one is possible) before checking the preconditions. + + +### Function: DualGovernance.scheduleProposal + +```solidity +function scheduleProposal(uint256 proposalId) +``` + +Instructs the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance to schedule the proposal with id `proposalId` for execution. + +#### Preconditions + +* The proposal with the given id MUST be already submitted. +* The proposal MUST NOT be scheduled. +* The proposal's dynamic timelock MUST have elapsed. +* The proposal MUST NOT be cancelled. +* The current governance state MUST be either `Normal` or `VetoCooldown`. + +Triggers a transition of the current governance state (if one is possible) before checking the preconditions. + +### Function: DualGovernance.tiebreakerApproveProposal + +```solidity +function tiebreakerApproveProposal(uint256 proposalId) +``` + +Marks the proposal with id `proposalId` as approved by the [Tiebreaker committee](#Tiebreaker-committee), given that the DG system is in a deadlock. + +#### Preconditions + +* MUST be called by the [Tiebreaker committee address](#Function-DualGovernancesetTiebreakerCommittee). +* Either the Tiebreaker Condition A or the Tiebreaker Condition B MUST be met (see the [mechanism design document][mech design - tiebreaker]). +* The proposal MUST be already submitted. +* The proposal MUST NOT be cancelled. +* The proposal with the specified id MUST NOT be already approved by the Tiebreaker committee. + +Triggers a transition of the current governance state (if one is possible) before checking the preconditions. + + +### Function: DualGovernance.tiebreakerScheduleProposal + +```solidity +function tiebreakerScheduleProposal(uint256 proposalId) +``` + +Instructs the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) 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](#Tiebreaker-committee) and that the tiebreaker execution timelock has elapsed. + +#### Preconditions + +* Either the Tiebreaker Condition A or the Tiebreaker Condition B MUST be met (see the [mechanism design document][mech design - tiebreaker]). +* The proposal MUST be already submitted. +* The proposal MUST NOT be cancelled. +* The proposal with the specified id MUST be approved by the Tiebreaker committee. +* The current block timestamp MUST be at least `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. + + +### Function: DualGovernance.cancelAllPendingProposals + +```solidity +function cancelAllPendingProposals() +``` + +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. + +#### Preconditions + +* MUST be called by an [admin proposer](#Administrative-actions). + + +### Function: DualGovernance.registerProposer + +```solidity +function registerProposer(address proposer, address executor) +``` + +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`](#Contract-Executorsol)) as an executor. + +#### Preconditions + +* MUST be called by the admin executor contract (see `Config.sol`). +* The `proposer` address MUST NOT be already registered in the system. +* The `executor` instance SHOULD be owned by the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance. + + +### Function: DualGovernance.unregisterProposer + +```solidity +function unregisterProposer(address proposer) +``` + +Removes the registered `proposer` address from the list of valid proposers and dissociates it with the executor contract address. + +#### Preconditions + +* MUST be called by the admin executor contract. +* The `proposer` address MUST be registered in the system as proposer. + + +### Function: DualGovernance.setTiebreakerCommittee + +```solidity +function setTiebreakerCommittee(address newTiebreaker) +``` + +Updates the address of the [Tiebreaker committee](#Tiebreaker-committee). + +#### Preconditions + +* MUST be called by the admin executor contract. + + +### Function: DualGovernance.activateNextState + +```solidity +function activateNextState() +``` + +Triggers a transition of the [global governance state](#Governance-state), if one is possible; does nothing otherwise. + + +## Contract: Executor.sol + +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`](#Contract-EmergencyProtectedTimelocksol) singleton instance. + +### Function: execute + +```solidity +function execute(address target, uint256 value, bytes payload) + payable returns (bytes result) +``` + +Issues a EVM call to the `target` address with the `payload` calldata, optionally sending `value` wei ETH. + +Reverts if the call was unsuccessful. + +#### Returns + +The result of the call. + +#### Preconditions + +* MUST be called by the contract owner (which SHOULD be the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance). + + +## Contract: Escrow.sol + +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: + +* The initial state is the `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. +* The final state is the `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](#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: + +1. The `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. +2. The `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. + +### Function: Escrow.lockStETH + +```solidity! +function lockStETH(uint256 amount) +``` + +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: + +```solidity +uint256 amountInShares = stETH.getSharesByPooledEther(amount); + +_vetoersLockedAssets[msg.sender].stETHShares += amountInShares; +_totalStEthSharesLocked += amountInShares; +``` + +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. + +#### Preconditions + +- The `Escrow` instance MUST be in the `SignallingEscrow` state. +- The caller MUST have an allowance set on the stETH token for the `Escrow` instance equal to or greater than the locked `amount`. +- The locked `amount` MUST NOT exceed the caller's stETH balance. + +### Function: Escrow.unlockStETH + +```solidity +function unlockStETH() +``` + +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: + +```solidity +_totalStEthSharesLocked -= _vetoersLockedAssets[msg.sender].stETHShares; +_vetoersLockedAssets[msg.sender].stETHShares = 0; +``` + +Additionally, the function triggers the `DualGovernance.activateNextState()` function at the beginning and end of the execution. + +#### Preconditions + +- The `Escrow` instance MUST be in the `SignallingEscrow` state. +- The caller MUST have a non-zero amount of previously locked stETH in the `Escrow` instance using the `Escrow.lockStETH` function. +- At least the duration of the `SignallingEscrowMinLockTime` MUST have passed since the caller last invoked any of the methods `Escrow.lockStETH`, `Escrow.lockWstETH`, or `Escrow.lockUnstETH`. + +### Function: Escrow.lockWstETH + +```solidity +function lockWstETH(uint256 amount) +``` + +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: + +```solidity +_vetoersLockedAssets[msg.sender].wstETHShares += amount; +_totalStEthSharesLocked += amount; +``` + +Finally, calls the `DualGovernance.activateNextState()` function. This action may transit the `Escrow` instance from the `SignallingEscrow` state into the `RageQuitEscrow` state. + +#### Preconditions + +- The `Escrow` instance MUST be in the `SignallingEscrow` state. +- The caller MUST have an allowance set on the wstETH token for the `Escrow` instance equal to or greater than the locked `amount`. +- The locked `amount` MUST NOT exceed the caller's wstETH balance. + +### Function: Escrow.unlockWstETH + +```solidity +function unlockWstETH() +``` + +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: + +```solidity +_totalStEthSharesLocked -= _vetoersLockedAssets[msg.sender].wstETHShares; +_vetoersLockedAssets[msg.sender].wstETHShares = 0; +``` + +Additionally, the function triggers the `DualGovernance.activateNextState()` function at the beginning and end of the execution. + +#### Preconditions + +- The `Escrow` instance MUST be in the `SignallingEscrow` state. +- The caller MUST have a non-zero amount of previously locked wstETH in the `Escrow` instance using the `Escrow.lockWstETH` function. +- At least the duration of the `SignallingEscrowMinLockTime` MUST have passed since the caller last invoked any of the methods `Escrow.lockStETH`, `Escrow.lockWstETH`, or `Escrow.lockUnstETH`. + + +### Function: Escrow.lockUnstETH + +```solidity +function lockUnstETH(uint256[] unstETHIds) +``` + +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: + +```solidity +uint256 amountOfShares = WithdrawalRequest[id].amountOfShares; + +_vetoersLockedAssets[msg.sender].withdrawalNFTShares += amountOfShares; +_totalWithdrawlNFTSharesLocked += amountOfShares; +``` + +Finally, calls the `DualGovernance.activateNextState()` function. This action may transition the `Escrow` instance from the `SignallingEscrow` state into the `RageQuitEscrow` state. + +#### Preconditions + +- The `Escrow` instance MUST be in the `SignallingEscrow` state. +- The caller MUST be the owner of all withdrawal NFTs with the given ids. +- The caller MUST grant permission to the `SignallingEscrow` instance to transfer tokens with the given ids (`approve()` or `setApprovalForAll()`). +- The passed ids MUST NOT contain the finalized or claimed Withdrawal NFTs. +- The passed ids MUST NOT contain duplicates. + +### Function: Escrow.unlockUnstETH + +```solidity +function unlockUnstETH(uint256[] unstETHIds) +``` + +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: + +- If the Withdrawal NFT was marked as finalized (see the `Escrow.markUnstETHFinalized()` function for details): + +```solidity +uint256 amountOfShares = WithdrawalRequest[id].amountOfShares; +uint256 claimableAmount = _getClaimableEther(id); + +_totalWithdrawlNFTSharesLocked -= amountOfShares; +_totalFinalizedWithdrawlNFTSharesLocked -= amountOfShares; +_totalFinalizedWithdrawlNFTAmountLocked -= claimableAmount; + +_vetoersLockedAssets[msg.sender].withdrawalNFTShares -= amountOfShares; +_vetoersLockedAssets[msg.sender].finalizedWithdrawalNFTShares -= amountOfShares; +_vetoersLockedAssets[msg.sender].finalizedWithdrawalNFTAmount -= claimableAmount; +``` + +- if the Withdrawal NFT wasn't marked as finalized: + +```solidity +uint256 amountOfShares = WithdrawalRequest[id].amountOfShares; + +_totalWithdrawlNFTSharesLocked -= amountOfShares; +_vetoersLockedAssets[msg.sender].withdrawalNFTShares -= amountOfShares; +``` + +Additionally, the function triggers the `DualGovernance.activateNextState()` function at the beginning and end of the execution. + +#### Preconditions + +- The `Escrow` instance MUST be in the `SignallingEscrow` state. +- Each provided Withdrawal NFT MUST have been previously locked by the caller. +- At least the duration of the `SignallingEscrowMinLockTime` MUST have passed since the caller last invoked any of the methods `Escrow.lockStETH`, `Escrow.lockWstETH`, or `Escrow.lockUnstETH`. + +### Function Escrow.markUnstETHFinalized + +```solidity +function markUnstETHFinalized(uint256[] unstETHIds, uint256[] hints) +``` + +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: + +- The value of the Withdrawal NFT is no longer affected by stETH token rebases. +- The total supply of stETH is adjusted based on the value of the finalized Withdrawal NFT. + +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`: + +```solidity +uint256 claimableAmount = _getClaimableEther(id); +uint256 amountOfShares = WithdrawalRequest[id].amountOfShares; + +_totalFinalizedWithdrawlNFTSharesLocked += amountOfShares; +_totalFinalizedWithdrawlNFTAmountLocked += claimableAmount; + +_vetoersLockedAssets[msg.sender].finalizedWithdrawalNFTShares += amountOfShares; +_vetoersLockedAssets[msg.sender].finalizedWithdrawalNFTAmount += claimableAmount; +``` + +Withdrawal NFTs belonging to any of the following categories are excluded from the rage quit support update: + +- Claimed or unfinalized Withdrawal NFTs +- Withdrawal NFTs already marked as finalized +- Withdrawal NFTs not locked in the `Escrow` instance + +#### Preconditions + +- The `Escrow` instance MUST be in the `SignallingEscrow` state. + +### Function Escrow.getRageQuitSupport() + +```solidity +function getRageQuitSupport() view returns (uint256) +``` + +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: + +```solidity +uint256 rebaseableAmount = stETH.getPooledEthByShares( + _totalStEthSharesLocked + + _totalWstEthSharesLocked + + _totalWithdrawalNFTSharesLocked - + _totalFinalizedWithdrawalNFTSharesLocked +); + +return 10 ** 18 * ( + rebaseableAmount + _totalFinalizedWithdrawalNFTAmountLocked +) / ( + stETH.totalSupply() + _totalFinalizedWithdrawalNFTAmountLocked +); +``` + +### Function Escrow.startRageQuit + +```solidity +function startRageQuit() +``` + +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. + +#### Preconditions + +- Method MUST be called by the `DualGovernance` contract. +- The `Escrow` instance MUST be in the `SignallingEscrow` state. + +### Function Escrow.requestNextWithdrawalsBatch + +```solidity +function requestNextWithdrawalsBatch(uint256 maxWithdrawalRequestsCount) +``` + +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. + +#### Preconditions + +- The `Escrow` instance MUST be in the `RageQuitEscrow` state. +- The `maxWithdrawalRequestsCount` MUST be greater than 0 +- The generation of WithdrawalRequest batches MUST not be concluded + +### Function Escrow.claimNextWithdrawalsBatch + +```solidity +function claimNextWithdrawalsBatch(uint256[] withdrawalRequestIds, uint256[] hints) +``` + +Allows 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. + +#### Preconditions + +- The `Escrow` instance MUST be in the `RageQuitEscrow` state. +- The `withdrawalRequestIds` array MUST contain only the ids of finalized but unclaimed withdrawal requests generated by the `Escrow.requestNextWithdrawalsBatch()` function. + +### Function Escrow.claimUnstETH + +```solidity +function claimUnstETH(uint256[] unstETHIds, uint256[] hints) +``` + +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. + +#### Preconditions + +- The `Escrow` instance MUST be in the `RageQuitEscrow` state. +- The provided `unstETHIds` MUST only contain finalized but unclaimed withdrawal requests with the owner set to `msg.sender`. + +### Function Escrow.isRageQuitFinalized + +```solidity +function isRageQuitFinalized() view returns (bool) +``` + +Returns whether the rage quit process has been finalized. The rage quit process is considered finalized when all the following conditions are met: +- The `Escrow` instance is in the `RageQuitEscrow` state. +- All withdrawal request batches have been claimed. +- The duration of the `RageQuitExtensionDelay` has elapsed. + +### Function Escrow.withdrawStEthAsEth + +```solidity +function withdrawStEthAsEth() +``` + +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: + +```solidity +return _totalClaimedEthAmount * _vetoersLockedAssets[msg.sender].stETHShares + / (_totalStEthSharesLocked + _totalWstEthSharesLocked); +``` + +#### Preconditions + +- The `Escrow` instance MUST be in the `RageQuitEscrow` state. +- The rage quit process MUST be completed, including the expiration of the `RageQuitExtensionDelay` duration. +- The `RageQuitEthClaimTimelock` period MUST be elapsed after the expiration of the `RageQuitExtensionDelay` duration. +- The caller MUST have a non-zero amount of stETH to withdraw. +- The caller MUST NOT have previously withdrawn stETH. + +### Function Escrow.withdrawWstEthAsEth + +```solidity +function withdrawWstEthAsEth() external +``` + +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: + +```solidity +return _totalClaimedEthAmount * + _vetoersLockedAssets[msg.sender].wstETHShares / + (_totalStEthSharesLocked + _totalWstEthSharesLocked); +``` + +#### Preconditions +- The `Escrow` instance MUST be in the `RageQuitEscrow` state. +- The rage quit process MUST be completed, including the expiration of the `RageQuitExtensionDelay` duration. +- The `RageQuitEthClaimTimelock` period MUST be elapsed after the expiration of the `RageQuitExtensionDelay` duration. +- The caller MUST have a non-zero amount of wstETH to withdraw. +- The caller MUST NOT have previously withdrawn wstETH. + +### Function Escrow.withdrawUnstETHAsEth + +```solidity +function withdrawUnstETHAsEth(uint256[] unstETHIds) +``` + +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. + +#### Preconditions + +- The `Escrow` instance MUST be in the `RageQuitEscrow` state. +- The rage quit process MUST be completed, including the expiration of the `RageQuitExtensionDelay` duration. +- The `RageQuitEthClaimTimelock` period MUST be elapsed after the expiration of the `RageQuitExtensionDelay` duration. +- The caller MUST be set as the owner of the provided NFTs. +- Each Withdrawal NFT MUST have been claimed using the `Escrow.claimUnstETH()` function. +- Withdrawal NFTs must not have been withdrawn previously. + + +## Contract: EmergencyProtectedTimelock.sol + +`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: + +1. The proposal must be submitted using the `EmergencyProtectedTimelock.submit` function. +2. The configured post-submit timelock must elapse. +3. The proposal must be scheduled using the `EmergencyProtectedTimelock.schedule` function. +4. The configured emergency protection timelock must elapse (can be zero, see below). +5. The proposal must be executed using the `EmergencyProtectedTimelock.execute` function. + +The contract only allows proposal submission and scheduling by the `governance` address. Normally, this address points to the [`DualGovernance`](#Contract-DualGovernancesol) 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: + +1. Clearing both the Emergency Activation and Execution Committees from the `EmergencyProtectedTimelock`. +2. Cancelling all proposals that have not been executed. +3. Setting the `governance` address to a pre-configured Emergency Governance address. In the simplest scenario, this would be the Lido DAO Aragon Voting contract. + +### Function: EmergencyProtectedTimelock.submit + +```solidity +function submit(address executor, ExecutorCall[] calls) + returns (uint256 proposalId) +``` + +Registers a new governance proposal composed of one or more EVM `calls` to be made by the `executor` contract. + +#### Returns + +The ID of the successfully registered proposal. + + +#### Preconditions + +* MUST be called by the `governance` address. + + +### Function: EmergencyProtectedTimelock.schedule + +```solidity +function schedule(uint256 proposalId) +``` + +#### Preconditions + +* MUST be called by the `governance` address. +* The proposal MUST be already submitted. +* The post-submit timelock MUST already elapse since the moment the proposal was submitted. + + +### Function: EmergencyProtectedTimelock.execute + +```solidity +function execute(uint256 proposalId) +``` + +Instructs the executor contract associated with the proposal to issue the proposal's calls. + +#### Preconditions + +* Emergency mode MUST NOT be active. +* The proposal MUST be already submitted & scheduled for execution. +* The emergency protection delay MUST already elapse since the moment the proposal was scheduled. + +### Function: EmergencyProtectedTimelock.cancelAllNonExecutedProposals + +```solidity +function cancelAllNonExecutedProposals() +``` + +Cancels all non-executed proposal, making them forever non-executable. + +#### Preconditions + +* MUST be called by the `governance` address. + +### Function: EmergencyProtectedTimelock.activateEmergencyMode + +```solidity +function activateEmergencyMode() +``` + +Activates the Emergency Mode. + +#### Preconditions + +* MUST be called by the Emergency Activation Committee address. +* The Emergency Mode MUST NOT be active. + +### Function: EmergencyProtectedTimelock.emergencyExecute + +```solidity +function emergencyExecute(uint256 proposalId) +``` + +Executes the scheduled proposal, bypassing the post-schedule delay. + +#### Preconditions + +* MUST be called by the Emergency Execution Committee address. +* The Emergency Mode MUST be active. + +### Function: EmergencyProtectedTimelock.deactivateEmergencyMode + +```solidity +function deactivateEmergencyMode() +``` + +Deactivates the Emergency Activation and Emergency Execution Committees (setting their addresses to `0x00`), cancels all unexecuted proposals, and disables the [Protected deployment mode](#Proposal-execution-and-deployment-modes). + +#### Preconditions + +* The Emergency Mode MUST be active. +* If the Emergency Mode was activated less than the `emergency mode max duration` ago, MUST be called by the [Admin Executor](#Administrative-actions) address. + +### Function: EmergencyProtectedTimelock.emergencyReset + +```solidity +function emergencyReset() +``` + +Resets the `governance` address to the `EMERGENCY_GOVERNANCE` value defined in the configuration, cancels all unexecuted proposals, and disables the [Protected deployment mode](#Proposal-execution-and-deployment-modes). + +#### Preconditions + +* The Emergency Mode MUST be active. +* MUST be called by the Emergency Execution Committee address. + +### Admin functions + +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](#Administrative-actions) address, basically routing any such changes through the Dual Governance mechanics. + + +## Contract: GateSealBreaker.sol + +In the Lido protocol, specific critical components (`WithdrawalQueue` and `ValidatorsExitBus`) are safeguarded by the `GateSeal` contract instance. According to the gate seals [documentation](https://github.com/lidofinance/gate-seals?tab=readme-ov-file#what-is-a-gateseal): + +>*"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: +- The minimum delay defined in the `GateSeal` contract has elapsed. +- Proposal execution is allowed within the dual governance system. + +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](https://github.com/lidofinance/core/blob/master/contracts/0.8.9/utils/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: + +- The `MIN_SEAL_DURATION_SECONDS` has elapsed since the committee activated the `GateSeal`. +- The `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. + +### Function GateSealBreaker.registerGateSeal + +```solidity +function registerGateSeal(IGateSeal gateSeal) +``` + +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. + +#### Preconditions + +- MUST be called by the contract owner (supposed to be set to Lido DAO). +- The `GateSeal` instance being registered MUST NOT have been previously registered. + +### Function GateSealBreaker.startRelease + +```solidity +function startRelease(IGateSeal gateSeal) +``` + +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`. + +#### Preconditions + +- The specified `gateSeal` MUST be registered in the contract. +- The `gateSeal` MUST be activated by the gate seal committee. +- The `MIN_SEAL_DURATION_SECONDS` MUST have passed since the activation of the `gateSeal`. +- The `gateSeal` MUST NOT be already released. +- The `DualGovernance` contract MUST be in either the `Normal` or `VetoCooldown` state. + +### Function GateSealBreaker.enactRelease + +```solidity +function enactRelease(IGateSeal gateSeal) +``` + +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. + +#### Preconditions + +- The `GateSealBreaker.startRelease()` function MUST be called for the specified `gateSeal`. +- The `RELEASE_DELAY` for the specified `gateSeal` MUST have elapsed since the release initiation. +- The `GateSealBreaker` contract SHOULD have been granted rights to unpause the sealed contracts. + +## Contract: Configuration.sol + +`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". + + +## Upgrade flow description + +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](#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: + +1. Set the `governance` variable in the `EmergencyProtectedTimelock` instance to the new version of the `DualGovernance` contract. +2. Update the implementation of the `Configuration` proxy contract if necessary. +3. Configure emergency protection settings in the `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.