-
Notifications
You must be signed in to change notification settings - Fork 82
feat: add edf to docs #962
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
chasingrainbows
wants to merge
20
commits into
main
Choose a base branch
from
feat/edf-docs
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
f391930
feat: add edf to docs
chasingrainbows 56e040c
fix: update hoodi factory address
chasingrainbows 831f5ed
chore: remove offline keys requirments
chasingrainbows 27fc241
feat: add new hoodi fabric address
chasingrainbows 6add8ea
fix: remove dsm v3 mention, council daemon supports v4 only
chasingrainbows 155ebe9
fix: replace personal tg handles with holders telegram chat
chasingrainbows 1f5028c
fix: allow multisig configurations stronger than 2-of-3
chasingrainbows 16f15d6
fix: remove distinct signers requirement from custody policy
chasingrainbows 34785fe
fix: remove unenforceable no-other-assets clause
chasingrainbows c0551a3
fix: clarify multisig signer set changes requirement
chasingrainbows 7cd37a2
fix: remove replacement key staging section
chasingrainbows 041023a
fix: simplify rotation procedure steps
chasingrainbows 217721d
fix: rework nominate, verify and fund rotation steps
chasingrainbows 679c91c
fix: apply review feedback to monitoring, cooldown and revoke sections
chasingrainbows ffbce3c
fix: apply remaining review feedback to edf guide and policy
chasingrainbows 8b689d9
fix: point owner address to multisig dashboard screenshot
chasingrainbows 997d8e9
Update key-custody-policy-for-edf-operators.md
chasingrainbows a0dbb72
Update key-custody-policy-for-edf-operators.md
chasingrainbows 5473d83
fix: add old EOA balance sweep step to retire-the-old-key sections
chasingrainbows 9813d27
Merge branch 'feat/edf-docs' of github.com:lidofinance/docs into featβ¦
chasingrainbows File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| # EDF Rotation and Incidents | ||
|
|
||
| Delegate key rotation and emergency procedures for an EDF seat. | ||
|
|
||
| - [EDF Operator Guide](./edf-operator-guide.md) β the setup | ||
| - [EDF Operator Key Custody Policy](./key-custody-policy-for-edf-operators.md) β the rules you must | ||
| follow | ||
|
|
||
| <details> | ||
| <summary><b>Example: running an Etherscan Hoodi transaction from a Safe wallet</b></summary> | ||
|
|
||
| `nominateDelegate`, `revokeDelegate` and `terminate` are `onlyOwner` β the caller must be the | ||
| multisig. A plain MetaMask connection sends them from your own EOA and they revert with `NotOwner`. | ||
|
|
||
| 1. Open your `DelegationContract` on Etherscan β **Contract** β **Write Contract** β | ||
| **Connect Wallet** β **WalletConnect** β **All Wallets**. A QR code appears β copy the pairing | ||
| link (`wc:...`) next to it. Do **not** pick MetaMask. | ||
| 2. In the Safe UI, click the **WalletConnect** icon in the header, paste the link into | ||
| **Pairing code**, and approve the session. The link expires within minutes β paste it right | ||
| after copying. | ||
|
|
||
|  | ||
|
|
||
| 3. Etherscan's header must now show the **multisig address**, not your EOA. | ||
| 4. Fill in the method and press **Write**. | ||
|
|
||
|  | ||
|
|
||
| 5. The call lands in the multisig queue. Signers confirm it with their own wallets, then anyone | ||
| executes it and pays the gas. | ||
|
|
||
|  | ||
|
|
||
| </details> | ||
|
|
||
| --- | ||
|
|
||
| ## Routine rotation | ||
|
|
||
| Rotate at least **once a year**; quarterly is recommended. Also rotate when an engineer with host or | ||
| secrets access leaves, when the host is rebuilt from an untrusted image, or when the key's history | ||
| is unknown. | ||
|
|
||
| 1. **Generate** the new key on the target host (step 1.1 of the guide applies). | ||
| 2. **Announce** at least **1 day** ahead on the research forum and in the operators' channel. | ||
| Oracle operators: also send the new delegate address to node operators for their | ||
| `ORACLE_ADDRESSES_ALLOWLIST`. | ||
| 3. **Stage it in the daemon**, keeping the current key in place: | ||
| - **Oracle:** set `MEMBER_PRIV_KEY_2` to the new key. Restart once. | ||
| - **Council:** set `WALLET_PRIVATE_KEY_2` to the new key, keeping `WALLET_PRIVATE_KEY` as it | ||
| is. Restart once. | ||
| 4. **Nominate** from the owner multisig, a day after the announcement: | ||
|
|
||
| ``` | ||
| nominateDelegate(<newDelegate>) | ||
| ``` | ||
|
|
||
| 5. **Verify your own nomination.** Read `getPendingDelegate()` on Etherscan, or: | ||
|
|
||
| ```bash | ||
| cast call <contract> "getPendingDelegate()(address,uint256)" --rpc-url $RPC_URL | ||
| ``` | ||
|
|
||
| The address and `activeFrom` must be exactly what you intended. | ||
| 6. **Fund the new address** β send it half of the current delegate's balance. | ||
| 7. **At `activeFrom`** the switch happens with no transaction and no restart. Verify: | ||
|
|
||
| ```bash | ||
| cast call <contract> "getDelegate()(address)" --rpc-url $RPC_URL # == new delegate | ||
| ``` | ||
|
|
||
| - Oracle: confirm a successful report in the following frame. | ||
| - Council: the log shows the new `delegateAddress`; confirm pings and messages continue. | ||
| 8. **Only after that succeeds, retire the old key:** | ||
| - **Oracle:** move the new key into `MEMBER_PRIV_KEY` and clear `MEMBER_PRIV_KEY_2`. Restart. | ||
| - **Council:** move the new key into `WALLET_PRIVATE_KEY` and clear `WALLET_PRIVATE_KEY_2`. | ||
| Restart. | ||
| - Delete the old key from your secrets store. | ||
| - Move the old address's remaining balance to the new delegate address. | ||
|
|
||
| Notes: | ||
|
|
||
| - Calling `nominateDelegate` again during the cooldown **replaces** the pending delegate and | ||
| **restarts** the 48 hours. | ||
| - It reverts if the address is zero, equals the owner, equals the current delegate, or equals the | ||
| pending delegate. | ||
| - Never stage a second key on a host you suspect is compromised. | ||
|
|
||
| ## Emergency: the delegate hot key may be compromised | ||
|
|
||
| Triggers: signatures or transactions you did not originate, host intrusion indicators, a secrets | ||
| store breach, malware on the host, or accidental disclosure (pasted in chat, committed to a repo, | ||
| captured in logs). | ||
|
|
||
| **Revoke first, investigate second.** | ||
|
|
||
| 1. From the owner multisig, call: | ||
|
|
||
| ``` | ||
| revokeDelegate() | ||
| ``` | ||
|
|
||
| It takes effect immediately and cancels any rotation in flight. | ||
| 2. **Notify** the holders' Telegram chat as soon as the transaction is sent: seat, revoked key, | ||
| known facts, and as much evidence as you can collect. | ||
| 3. **Re-key on clean infrastructure**: new key on a rebuilt or verified host, funded, added to the | ||
| daemon config, then `nominateDelegate(newKey)` from the multisig. The seat comes back **48 hours | ||
| later**. | ||
| 4. **Publish a post-incident report** (timeline, root cause, exposure window, custody changes) on | ||
| the forum, or in the holders' Telegram chat if disclosure is sensitive. | ||
|
|
||
| While revoked, the Council daemon logs `DelegationContract 0x⦠has no active delegate` every block | ||
| and the Oracle logs a warning each cycle. This stops once the new delegate activates. | ||
|
|
||
| ## Emergency: the owner multisig may be compromised | ||
|
|
||
| Triggers: unexpected changes to the multisig participants, unexpected multisig activity, or a | ||
| compromised signer device with any doubt about the rest of the quorum. | ||
|
|
||
| 1. If the owner itself can no longer be trusted, call from the multisig: | ||
|
|
||
| ``` | ||
| terminate() | ||
| ``` | ||
|
|
||
| **This is irreversible.** It disables `execute()`, fails all signature verification closed, and | ||
| clears the delegate forever. | ||
| 2. **Notify governance and the holders' Telegram chat immediately.** Restoring the seat needs a *new* | ||
| `DelegationContract` with a *new* owner multisig **and a governance vote**. | ||
|
|
||
| You have exactly one cooldown (48 h) between a hostile `DelegateNominated` and it becoming | ||
| effective. |
236 changes: 236 additions & 0 deletions
236
docs/guides/edf/key-custody-policy-for-edf-operators.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,236 @@ | ||
| # EDF Operator Key Custody Policy | ||
|
|
||
| > π Policy for [LIP-37: Execution Delegation Framework](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-37.md). Applies to operators of permissioned roles behind an EDF `DelegationContract`, initially Lido Oracle committee members and DSM guardians. | ||
|
|
||
| **Version:** 1.0 | ||
|
|
||
| **Applies to:** Operators of permissioned roles behind an EDF `DelegationContract` | ||
|
|
||
| **Maintained:** On the Lido research forum; may be revised without a protocol change | ||
|
|
||
| The key words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, and **MAY**, when they appear in uppercase, are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174). | ||
|
|
||
| --- | ||
|
|
||
| ## 1. Purpose and scope | ||
|
|
||
| LIP-37 moves key rotation from a ~10-day governance vote to a local operator action. | ||
|
|
||
| That only improves security if key holders store, rotate, and revoke keys with discipline. This document defines that discipline. | ||
|
|
||
| It covers: | ||
|
|
||
| - The two key classes in the EDF model | ||
| - Custody requirements for each key class | ||
| - Rotation cadence | ||
| - Required response to suspected or confirmed compromise | ||
|
|
||
| --- | ||
|
|
||
| ## 2. Key classes | ||
|
|
||
| | Key | Role in EDF | Exposure | Custody class | | ||
| | --- | --- | --- | --- | | ||
| | **Owner key** | Controls the `DelegationContract`: `nominateDelegate()`, `revokeDelegate()`, `terminate()` | Used rarely for rotations and incidents | **Cold** β Safe multisig; hardware cold-wallet signers RECOMMENDED | | ||
| | **Delegate key** | Hot signing key stored in and used by the off-chain daemon | Online continuously; assumed compromisable | **Hot** β machine-resident, minimized blast radius, rotated routinely | | ||
|
|
||
| The two classes have opposite design goals: | ||
|
|
||
| - The **owner key** is the security boundary of the whole model. It must be nearly impossible to steal, even at the cost of being slow to use. | ||
| - The **delegate key** is expected to be exposed by its nature. The policy goal is not to make it unstealable, but to keep it worthless quickly through narrow permissions, short lifetime, and instant revocation. | ||
|
|
||
| --- | ||
|
|
||
| ## 3. Owner key custody | ||
|
|
||
| The owner address is fixed at deployment and cannot be changed on-chain. Replacing it means deploying a new `DelegationContract` and passing a governance vote to reassign the seat. | ||
|
|
||
| Treat the owner setup as a long-lived commitment and get it right before deployment. | ||
|
|
||
| 1. **The owner MUST be a Safe multisig.** | ||
|
|
||
| A bare EOA owner is not acceptable for Oracle or DSM seats. | ||
|
|
||
| 2. **The owner MUST be dedicated to its assigned activity.** | ||
|
|
||
| The multisig MUST be responsible only for the single activity it was assigned to perform: key delegation management. It MUST NOT be used for any other purpose. | ||
|
|
||
| 3. **The multisig MUST be at least 2-of-3.** | ||
|
|
||
| A higher threshold or more signers is acceptable. | ||
|
|
||
| 4. **Every signer SHOULD be a hardware cold wallet.** | ||
|
|
||
| Hardware cold wallets, such as Ledger or Trezor, are RECOMMENDED for all signers. | ||
|
|
||
| 5. **Multisig signer set changes MUST be executed promptly.** | ||
|
|
||
| Every signer set change SHOULD be paired with a hot key rotation. | ||
|
|
||
| - **Departure or role change.** No later than the personβs last day of access. Removing the signer MUST NOT delay revocation of their other access. | ||
| - **Lost or stolen signer device, or exposed seed backup.** Within **24 hours** of the loss being reported. If the affected signer together with any other doubtful signer would meet the threshold, treat it as a Β§6.2 event. | ||
| - **Suspected compromise of the signerβs computer, or coercion.** Within **24 hours**. | ||
| - **Routine device replacement, or a signer who cannot be reached out of hours.** Within **5 business days**. | ||
|
|
||
|
|
||
| --- | ||
|
|
||
| ## 4. Delegate hot-key custody | ||
|
|
||
| 1. **Use one key per seat and environment.** | ||
|
|
||
| A delegate key MUST be unique to a single `DelegationContract` and a single environment. It MUST NOT be reused across mainnet/testnet, across Oracle and Council daemons, or for anything besides its seatβs duties. | ||
|
|
||
| 2. **Harden the host.** | ||
|
|
||
| The daemon host SHOULD be dedicated to the role, with: | ||
|
|
||
| - Access limited to named engineers | ||
| - Audited access channels | ||
| - No shared SSH accounts | ||
| - Current OS and daemon versions | ||
| - No unrelated internet-facing services | ||
| 3. **Keep only minimal balance.** | ||
|
|
||
| The delegate address MUST hold only working gas funds. A low-balance alert SHOULD be configured. | ||
|
|
||
| 4. **Delegate keys MUST be dedicated to their assigned activity.** | ||
|
|
||
| Each hot key MUST be responsible only for the single activity it was assigned to perform (day-to-day protocol operation). It MUST NOT be used for any other purpose. | ||
|
|
||
|
|
||
| --- | ||
|
|
||
| ## 5. Rotation policy | ||
|
|
||
| EDF makes rotation seamless: `nominateDelegate(newKey)` keeps the old key effective until the new one activates after the cooldown. | ||
|
|
||
| This section applies to delegate rotations after the EDF migration is complete. During the initial migration, the existing hot EOA is configured as the initial delegate and is effective immediately; the governance action reassigning the seat from that EOA to its `DelegationContract` is the migration cutover. | ||
|
|
||
| A `DelegationContract` authorizes exactly one effective delegate at a time. Before `activeFrom`, the current delegate remains effective. Starting at `activeFrom`, the nominated delegate becomes effective automatically and the previous EOA loses its authority through that `DelegationContract`, although the EOA itself continues to exist. | ||
|
|
||
| 1. **Routine cadence** | ||
|
|
||
| The delegate key MUST be rotated at least every **1 year**. Quarterly rotation is RECOMMENDED. | ||
|
|
||
| 2. **Event-driven rotation** | ||
|
|
||
| Independent of cadence, the delegate key MUST be rotated when: | ||
|
|
||
| - An engineer with access to the daemon host or secrets store leaves the organization or changes role | ||
| - The daemon host is migrated or rebuilt from an untrusted image | ||
| - Any dependency or infrastructure incident could have exposed the key | ||
| - The keyβs age or custody history is unknown | ||
|
|
||
| If exposure is suspected rather than merely possible, this becomes revocation, not rotation. | ||
|
|
||
| 3. **Announce rotations** | ||
|
|
||
| Routine rotations MUST be announced on the Lido research forum at least **1 day** before `nominateDelegate()` is executed and in the operatorsβ coordination channel before execution. This lets monitoring parties distinguish a planned `DelegateNominated` from a hostile one. | ||
|
|
||
| 4. **Planned rotation procedure** | ||
| 1. Generate the new key. | ||
| 2. Publish the pre-nomination announcement on the research forum. | ||
| 3. Add the replacement key to the daemon as its staged secondary member key. Keep the current delegate configured and operating. | ||
| 4. On behalf of the owner, execute `nominateDelegate(newKey)` on your `DelegationContract`. The old key remains effective during the cooldown. | ||
| 5. Watch for your own `DelegateNominated` event and verify that the delegate and `activeFrom` returned by `getPendingDelegate()` match the intended rotation. | ||
| 6. Fund the replacement address from the current delegate address with half of its balance. | ||
| 7. During the cooldown, the daemon MUST continue using the current delegate. | ||
| 8. After activation: | ||
| - Verify `getDelegate() == newKey`. | ||
| - Confirm that the daemon selected the new key. | ||
| - Confirm a successful report or message in the following applicable frame. | ||
| 9. Only after successful verification: | ||
| - Remove the previous key from the daemon configuration and secrets store. | ||
| - Move the previous EOAβs remaining balance to the new delegate address. | ||
| 5. **Owner rotation** | ||
|
|
||
| Multisig signer keys follow Β§3.5. Replacing the multisig itself requires a new `DelegationContract` deployment and a governance vote. | ||
|
|
||
|
|
||
| --- | ||
|
|
||
| ## 6. Incident response | ||
|
|
||
| Speed is the point of EDF. The contract lets operators drop a key in one transaction; this section defines when they must. | ||
|
|
||
| ### 6.1 Suspected or confirmed delegate hot-key compromise | ||
|
|
||
| Triggers include: | ||
|
|
||
| - Signatures or transactions you did not originate | ||
| - Host intrusion indicators | ||
| - Secrets-store breach | ||
| - Malware on the daemon host | ||
| - Accidental key disclosure, such as pasting in chat, committing to a repo, or capturing in logs | ||
|
|
||
| Response: | ||
|
|
||
| 1. **Revoke first, investigate second.** | ||
|
|
||
| The owner MUST call `revokeDelegate()` immediately upon suspicion. | ||
|
|
||
| Revocation takes effect immediately: it clears both the current delegate and any pending one, and signature verification through the contract fails closed from that moment on. If a rotation is in flight, revocation cancels it β the staged replacement must be nominated again once the seat is safe to restore. | ||
|
|
||
| 2. **Notify security and operators.** | ||
|
|
||
| Notify the holdersβ Telegram chat as soon as the revocation transaction is sent. Include: | ||
|
|
||
| - Seat | ||
| - Revoked key | ||
| - Known facts | ||
| - As much evidence as you can collect | ||
| 3. **Re-key on clean infrastructure.** | ||
|
|
||
| Generate a replacement per Β§4 on a host you trust, rebuilt or verified clean, and call `nominateDelegate(newKey)`. The seat resumes after the cooldown. | ||
|
|
||
| 4. **Publish a post-incident report.** | ||
|
|
||
| Publish a summary to the research forum, or to the holdersβ Telegram chat if disclosure is sensitive. Include: | ||
|
|
||
| - Timeline | ||
| - Root cause | ||
| - Exposure window | ||
| - Custody changes made | ||
|
|
||
| ### 6.2 Suspected owner cold-key / multisig compromise | ||
|
|
||
| Triggers include: | ||
|
|
||
| - Unexpected changes to the multisig participants | ||
| - Unexpected multisig activity | ||
| - A compromised signer device combined with any doubt about the rest of the quorum | ||
|
|
||
| Response: | ||
|
|
||
| 1. **If the owner itself can no longer be trusted:** | ||
|
|
||
| The owner MUST call `terminate()`. | ||
|
|
||
| Termination is irreversible. It disables `execute()`, fails all signature verification closed, and clears the delegate. A dead seat is strictly better than a stolen one. | ||
|
|
||
| 2. **Notify immediately.** | ||
|
|
||
| Notify the holdersβ Telegram chat immediately. Governance will need to reassign the seat to a freshly deployed `DelegationContract` with a new owner multisig, so early notice shortens downtime. | ||
|
|
||
|
|
||
| --- | ||
|
|
||
| ## 7. Monitoring | ||
|
|
||
| Alongside Lidoβs protocol-wide monitoring, each operator SHOULD independently monitor their own contract. | ||
|
|
||
| ### Recommended alerts | ||
|
|
||
| - **`DelegateNominated`, `DelegateRevoked`, and `Terminated` events** on the operatorβs `DelegationContract` | ||
| - SHOULD alert a human 24/7 | ||
| - An unexpected `DelegateNominated` is the primary owner-compromise signal | ||
| - The owner MUST react to an unexpected nomination before the cooldown elapses | ||
| - **Delegate address activity** outside the daemonβs expected pattern | ||
| - Unexpected `execute()` targets, including EOA destinations | ||
| - Unexpected non-zero `msg.value` forwarded through `execute()` | ||
| - Transactions from the delegate EOA itself | ||
|
chasingrainbows marked this conversation as resolved.
|
||
|
|
||
| ### Emergency contact | ||
|
|
||
| Each operator MUST provide a fast contact channel for emergencies, where a human can be reached at any time, and MUST keep it current. | ||
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.