LIP-37: Execution Delegation Framework (EDF)

Seat: DSM guardian (Lido council daemon)
DelegationContract: 0x915F0Fa50E1af761B113b41c79ab33Bf4734C36E
Owner multisig: 0x2E6e175F57D7a18b3CA5490844034600FC18EF83
Delegate EOA: 0x5ecf14e7a3831D44e2F7d4542Ba98C6c96E47420

  • I have read the EDF Operator Key Custody Policy and my setup follows it
  • I created a dedicated owner multisig, at least 2-of-3, with signers held by different people
    on different devices
  • The multisig is used for nothing except this delegation contract
  • I deployed my DelegationContract from the official DelegationFactory, with a 48-hour
    (172800 s) cooldown
  • I verified on-chain that owner(), getDelegate(), getCooldown() and isTerminated() are what I intended
  • I set up 24/7 alerts on DelegateNominated, DelegateRevoked and Terminated

Operator : Bitwise
Seat :
Lido Oracle
DelegationContract : 0x56b3ea8016da18c6e8cd8135492d242f0de0dbbc
Owner multisig. : 0x6057526Da2Dc9Bd23a3C6F1b15C74De2D7593378
Delegate EOA. : 0x5416CAAb6f37BF81cb2dc7ce0a363AfB9DD92d57

- [x] I have read the EDF Operator Key Custody Policy and my setup follows it
- [x] I created a dedicated owner multisig, at least 2-of-3, with signers held by different people on different devices
- [x] The multisig is used for nothing except this delegation contract
- [x] I deployed my DelegationContract from the official DelegationFactory, with a 48-hour(172800 s) cooldown
- [x] I verified on-chain that owner(), getDelegate(), getCooldown() and isTerminated() are what I intended
- [x] I set up 24/7 alerts on DelegateNominated, DelegateRevoked and Terminated
1 Like

Operator: bloXroute
Seat: Lido Oracle
DelegationContract: 0x99Cd2EF33040879D40BBC77Df81863D97f13C64d
Owner multisig: 0x9462A9FfF5646d0b6517D8eac49de49277e89dB9
Delegate EOA: 0x375ABa35EA2011Af97b51bd395494C827d1C39BD

I have read the EDF Operator Key Custody Policy and my setup follows it
I created a dedicated owner multisig, at least 2-of-3, with signers held by different people on different devices
The multisig is used for nothing except this delegation contract
I deployed my DelegationContract from the official DelegationFactory, with a 48-hour (172800 s) cooldown
I verified on-chain that owner(), getDelegate(), getCooldown() and isTerminated() are what I intended
I set up 24/7 alerts on DelegateNominated, DelegateRevoked and Terminated

2 Likes

Seat: Lido Depositor Bot
DelegationContract: 0x6Aa249bA53A3abcaC52F91146583B3eE2Ee4C7F5
Owner multisig: 0x2E6e175F57D7a18b3CA5490844034600FC18EF83
Delegate EOA: 0x2df4013EF30b09100A027192E700114cD0D13900

  • I have read the EDF Operator Key Custody Policy and my setup follows it
  • I created a dedicated owner multisig, at least 2-of-3, with signers held by different people
    on different devices
  • The multisig is used for nothing except this delegation contract
  • I deployed my DelegationContract from the official DelegationFactory, with a 48-hour
    (172800 s) cooldown
  • I verified on-chain that owner(), getDelegate(), getCooldown() and isTerminated() are what I intended
  • I set up 24/7 alerts on DelegateNominated, DelegateRevoked and Terminated

Hi everyone!

The core contracts for the EDF upgrade have been deployed on Ethereum mainnet:

  • DepositSecurityModule v5: 0x39BB5d491e98A44D1bfe8047A737a81E296a63E0
  • LidoLocator implementation: 0x60E09F1791F1168d0450E4F100616B4a3F95119C

Deployed from lidofinance/core, branch feat/edf, commit e4d0404: core/deployed-mainnet.json at e4d0404c85b043b1b9fb1dd42d85c2535a7f30d8 · lidofinance/core · GitHub

The new DSM is configured with 6 guardian DelegationContracts and a quorum of 4. Both contracts will be activated by the upcoming on-chain vote; until then the current DSM and Locator implementation stay in use.

1 Like

Upgrade process

The vote is a single Aragon vote with two items.

Item 1 submits one Dual Governance proposal that atomically switches the protocol to EDF and DepositSecurityModule v5 in a single Agent.forward call (78 actions):

  • for each of the four oracle committees and each of the nine oracle operators: remove the operator’s EOA hot key from the HashConsensus and add the operator’s DelegationContract, keeping the quorum of 5 (72 actions);
  • upgrade the LidoLocator implementation so that its depositSecurityModule entry points to the new DSM v5, all other entries stay the same (1 action);
  • move STAKING_MODULE_UNVETTING_ROLE on StakingRouter from DSM v4 to DSM v5 (2 actions);
  • move TOP_UP_ROLE on TopUpGateway from the depositor bot EOA to the depositor bot DelegationContract (2 actions);
  • grant BUFFER_RESERVE_MANAGER_ROLE on Lido to the Easy Track EVMScriptExecutor (1 action).

Item 2 is executed by the Aragon Voting directly: add the SetDepositsReserveTarget Easy Track factory with the permission limited to Lido.setDepositsReserveTarget(uint256), per the CMC deposit reserve target proposal.

DSM v5 is already deployed with the guardian set moved to DelegationContracts (see the deployment post), so the vote does not touch the guardians: it only switches the protocol to the new module. The guardian quorum stays 4 of 6, Stakely takes the seat of Kiln.

Oracle committees

# Committee HashConsensus Quorum
1 AccountingOracle 0xD624B08C83bAECF0807Dd2c6880C3154a5F0B288 5
2 ValidatorsExitBusOracle 0x7FaDB6358950c5fAA66Cb5EB8eE5147De3df355a 5
3 CSFeeOracle 0x71093efF8D8599b5fA340D665Ad60fA7C80688e4 5
4 Curated Module FeeOracle 0x902D64c93F6595339aA46105627a085591051aFb 5

Oracle members (the same rotation on every committee)

DSM guardians (already set on DSM v5, not changed by the vote)

Contracts and roles

What Before After
DepositSecurityModule (LidoLocator entry) v4 0xF573E9E3de1f86B085417ab294f56E7920B4e9Be v5 0x39BB5d491e98A44D1bfe8047A737a81E296a63E0
LidoLocator implementation 0xF2Ffb952e129a63F0614Ff87126E1d4a494A2313 0x60E09F1791F1168d0450E4F100616B4a3F95119C
STAKING_MODULE_UNVETTING_ROLE on StakingRouter DSM v4 DSM v5
TOP_UP_ROLE on TopUpGateway depositor bot EOA 0xF82aC5937A20dC862F9bc0668779031E06000f17 depositor bot DelegationContract 0x6Aa249bA53A3abcaC52F91146583B3eE2Ee4C7F5
BUFFER_RESERVE_MANAGER_ROLE on Lido Agent Agent and Easy Track EVMScriptExecutor 0xFE5986E06210aC1eCC1aDCafc0cc7f8D63B3F977
Easy Track factories - SetDepositsReserveTarget 0x62E9Dc68BDCBC46362f40e0bb9c154C9a42E62b0, trusted caller CMC multisig, cap 9,600 ETH

The vote script and the fork tests: lidofinance/scripts#682.

5 Likes

Following the single-stage vote process, the vote text for LIP-37: Execution Delegation Framework (EDF) has been finalized and frozen on IPFS - this is the exact text the DAO will vote on.

The on-chain vote is planned to start on Wednesday, September 16. Once the vote is live, a link to it and instructions for verifying the vote items will follow in this thread.

2 Likes

EDF audits finalized:

4 Likes

The on-chain vote #205 is now live and includes the proposals to complete the LIP-37: Execution Delegation Framework (EDF) rollout.

The vote will be open for your “For” or “Against” input until the end of the main phase: Sept 21, 13:45 UTC.

For instructions on how to verify the vote items, please follow this guide.

1 Like

We verified the vote script and all sixteen DelegationContracts on-chain before voting:
nine oracle seats, six DSM guardians, and the depositor bot’s contract, which takes over
TOP_UP_ROLE in the same script. Everything matches the published plan and the custody
policy. Thanks for the level of preparation here: the published tables, the operator
checklists and the verification guide made this vote unusually easy to check. The
question below is not about the implementation but about one part of the model that we
may be missing.

It concerns the case where what is compromised is not a hot key but the owner multisig
itself. TheDZhon made the point earlier in this thread that terminate() lets a
participant cut itself off rather than having the protocol pause components around a
party it no longer trusts. We agree, and our question is about the narrower case where
the owner is no longer in a position to make that call. terminate() is invoked by the
owner, as are revokeDelegate() and nominateDelegate(). While a single signer is
compromised this works, because the remaining honest quorum still controls the Safe.
Once the threshold itself is taken, the operator can do nothing, and §6.2 of the policy
no longer covers the case.

From there governance has to act. The LIP-37 specification describes the cooldown in
exactly those terms: a hostile nomination is visible on-chain for the whole window, so
monitoring and governance can react before the swap takes effect. The custody policy in
§3.6 calls the same window the owner’s only window to react, which is noticeably more
modest.

We tried to work out what governance can actually do within those 48 hours, and so far
the answer seems to be nothing. We checked the roles on the four HashConsensus contracts:

  • MANAGE_MEMBERS_AND_QUORUM_ROLE is held only by the Aragon Agent
  • DISABLE_CONSENSUS_ROLE has no holders at all, zero on all four committees
  • MANAGE_FRAME_CONFIG_ROLE is empty as well

CircuitBreaker covers the Withdrawal Queue, VEBO, TWG, VaultHub and Predeposit Guarantee,
but not the HashConsensus contracts or the Accounting Oracle. Emergency Brakes holds
PAUSE_ROLE over Easy Track. The Dual Governance emergency committees are scoped to
vulnerabilities in the DG contracts themselves, not to changing committee membership.
That leaves a full Aragon vote through the Agent plus the Dual Governance timelock,
roughly ten days. This is the same figure LIP-37 names in its motivation as the problem
EDF was created to solve. So the window lasts 48 hours while the fastest available
response is five times longer.

There is also a faster version of the same problem. An attacker holding owner multisigs
does not need to nominate anything: revokeDelegate() and terminate() both take effect
immediately. Five disabled oracle seats leave four signers against a quorum of five, and
because each operator’s contract sits in all four committees, consensus stops in all of
them at once. Here there is no 48-hour window at all. Restoring service means either
lowering the quorum or replacing members, both of which run through
MANAGE_MEMBERS_AND_QUORUM_ROLE and therefore through a full vote; with terminate() it
also means deploying a new contract per seat. Even a single terminated seat is
unrecoverable without a vote.

These are two different failure modes and they need two different primitives. Against a
hostile majority, disableConsensus() freezes the committee by raising the quorum out of
reach. But DISABLE_CONSENSUS_ROLE is held by nobody, and granting it goes through the
Agent, which means a vote again. It becomes a fast brake only if it sits in advance with
a committee that can act within hours. Against disabled seats it does not help at all,
since consensus has already stopped and freezing it further changes nothing. Recovery
needs removeMember or setQuorum, a far more sensitive role to hand to a fast-moving
committee.

DSM guardians do not have this problem, since any honest guardian can halt deposits with
a single signature, and that is built into the design as protection against collusion.
For oracle seats we could not find an equivalent emergency path. Are we right that until
removal through governance, the only containment mechanism at the HashConsensus level is
the 5-of-9 quorum? We do take downstream limits such as the OracleReportSanityChecker
into account, but they bound what a report may contain rather than allow a compromised
committee member to be removed or disabled.

We deliberately do not go into how an owner multisig might be compromised. The method
does not matter here: custody requirements raise the cost but do not guarantee it will
not happen.

None of these questions changes our support for the vote. Holding a protocol permission
directly on a hot key that cannot be replaced quickly without governance is worse on
every one of these counts. We are not proposing to redesign anything. We would like the
response path for a threshold compromise of an owner multisig to be defined in advance:
who neutralises a compromised oracle seat, and by what mechanism, when the owner is
already lost and the normal governance process does not fit inside the 48-hour cooldown.

1 Like

The new framework addresses exactly the issues Raman outlined at the beginning of this post.

Under the EDF framework, only the delegate key is stored on each holder’s machine. If a machine running the Oracle or Council Daemon is compromised, this key can be quickly replaced through the Delegation Contract. This significantly improves the security model by eliminating the need for key rotation through a full on-chain governance vote, which takes approximately 10 days.

The risk of cold-key compromise remains, but it is now significantly lower. As a result, the risk of losing quorum or even having the quorum compromised is also substantially reduced. However, the mitigation process for such severe incidents remains the same as it was before the EDF release.

3 Likes

The on-chain vote #205 has been enacted. The EDF rollout will follow the Dual Governance Proposal #14 execution, expected on September 25th.

2 Likes

Dual Governance Proposal #14 has been executed - the EDF rollout has been completed.

2 Likes