diff --git a/docs/contracts/delegation-contract.md b/docs/contracts/delegation-contract.md new file mode 100644 index 000000000..6ccb1df86 --- /dev/null +++ b/docs/contracts/delegation-contract.md @@ -0,0 +1,209 @@ +# DelegationContract + +- [Source Code](https://github.com/lidofinance/execution-delegation-framework/blob/main/src/DelegationContract.sol) +- [Audit](https://github.com/lidofinance/audits/blob/main/Composable%20Security%20Lido%20EDF%20On-chain%20Audit%20Report%2008-2026.pdf) +- \[[proposed](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/25)\] Deployed instances: [Lido Oracle](/holders/lido-oracle) and [Lido Council Daemon](/holders/lido-council-daemon) members + +`DelegationContract` is the per-seat contract of the [Execution Delegation Framework (EDF)](/guides/edf/edf-operator-guide). It has one owner and one active delegate. The owner is a cold multisig that nominates and revokes the delegate. The delegate is a hot key that does the daily work: it sends transactions through `execute()` or signs messages that the protocol checks through ERC-1271 `isValidSignature()`. + +The contract holds a protocol seat instead of an EOA: a `HashConsensus` member for the Lido Oracle, a [`DepositSecurityModule`](/contracts/deposit-security-module) guardian, or the depositor. The owner cannot call `execute()` or sign for the contract. + +- Owner and cooldown are set in the constructor and cannot be changed. To change the owner, deploy a new contract from the [`DelegationFactory`](/contracts/delegation-factory) and move the seat by a governance vote. +- A nominated delegate becomes active only after the cooldown. The current delegate stays active until then, so a hostile nomination is visible before it takes effect. +- Revocation and termination are immediate. Termination is permanent. +- The contract cannot receive ETH. If the target sends ETH back, `execute()` reverts. + +## View Methods + +### owner() + +Returns the owner address (ERC-5313). + +```solidity +function owner() external view returns (address); +``` + +### getDelegate() + +Returns the active delegate, or zero address if there is none: never nominated, revoked, or terminated. A nominated delegate is returned only after its cooldown has passed. + +```solidity +function getDelegate() external view returns (address); +``` + +### getPendingDelegate() + +Returns the pending delegate and the timestamp when it becomes active, or `(address(0), 0)` if there is no pending nomination. After `activeFrom` the pending delegate becomes the active one without any transaction. + +```solidity +function getPendingDelegate() external view returns (address delegate, uint256 activeFrom); +``` + +### getCooldown() + +Returns the cooldown in seconds between a nomination and the moment the new delegate becomes active. + +```solidity +function getCooldown() external view returns (uint256); +``` + +### isTerminated() + +Returns whether the contract is terminated. + +```solidity +function isTerminated() external view returns (bool); +``` + +### isValidSignature() + +ERC-1271 check. Returns `0x1626ba7e` if `signature` is a valid ECDSA signature of `hash` by the active delegate, and `0xffffffff` otherwise. Always fails when there is no active delegate. + +```solidity +function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4 magicValue); +``` + +:::note +The result depends on the contract state. A signature that is valid now becomes invalid after the delegate is rotated or revoked, or the contract is terminated. +::: + +#### Parameters + +| Name | Type | Description | +| ----------- | --------- | ---------------------------- | +| `hash` | `bytes32` | Message hash that was signed | +| `signature` | `bytes` | ECDSA signature | + +### supportsInterface() + +ERC-165 check. Returns `true` for the ERC-165, ERC-1271, ERC-5313 and `IDelegationContract` interface ids. + +```solidity +function supportsInterface(bytes4 interfaceId) external pure returns (bool); +``` + +## Methods + +### nominateDelegate() + +Nominates a new delegate. It becomes active after `getCooldown()` seconds. The current delegate stays active until then. A new nomination during the cooldown replaces the pending delegate and restarts the cooldown. + +```solidity +function nominateDelegate(address delegate) external; +``` + +:::note +Reverts if any of the following is true: + +- `msg.sender` is not the owner; +- the contract is terminated; +- `delegate` is zero address; +- `delegate` is the owner; +- `delegate` is the active delegate; +- `delegate` is the pending delegate. +::: + +#### Parameters + +| Name | Type | Description | +| ---------- | --------- | -------------------- | +| `delegate` | `address` | New delegate address | + +### revokeDelegate() + +Immediately removes the active and the pending delegate. + +```solidity +function revokeDelegate() external; +``` + +:::note +Reverts if any of the following is true: + +- `msg.sender` is not the owner; +- the contract is terminated. +::: + +### terminate() + +Terminates the contract: disables `execute()`, `isValidSignature()` and `nominateDelegate()` forever and removes the active and pending delegate. Intended for the case when the owner itself may be compromised. The seat then has to be moved to a new contract by a governance vote. + +```solidity +function terminate() external; +``` + +:::note +Reverts if any of the following is true: + +- `msg.sender` is not the owner; +- the contract is already terminated. +::: + +### execute() + +Calls `target` with `data` on behalf of the contract. The target sees the contract as `msg.sender`. `msg.value` is forwarded. If the call fails, the revert reason is passed through. + +```solidity +function execute(address target, bytes calldata data) external payable returns (bytes memory result); +``` + +:::note +Reverts if any of the following is true: + +- `msg.sender` is not the active delegate; +- the contract is terminated; +- `target` is zero address; +- `target` is the contract itself; +- the target call reverts. +::: + +#### Parameters + +| Name | Type | Description | +| -------- | --------- | --------------- | +| `target` | `address` | Address to call | +| `data` | `bytes` | Call data | + +#### Returns + +| Name | Type | Description | +| -------- | ------- | --------------------------- | +| `result` | `bytes` | Return data of the call | + +## Events + +### InitialDelegateSet() + +Emitted at deployment when the initial delegate is not zero address. + +```solidity +event InitialDelegateSet(address indexed newDelegate); +``` + +### DelegateNominated() + +Emitted on `nominateDelegate()`. `activeFrom` is the timestamp when the new delegate becomes active. + +```solidity +event DelegateNominated(address indexed newDelegate, uint256 activeFrom); +``` + +### DelegateRevoked() + +Emitted on `revokeDelegate()`. `revokedDelegate` is the delegate that was active, or zero address if there was none. + +```solidity +event DelegateRevoked(address indexed revokedDelegate); +``` + +### Terminated() + +Emitted on `terminate()`. + +```solidity +event Terminated(); +``` + +:::note +`execute()` emits no event. Monitor delegate activity through internal transactions of the contract, see the [operator guide](/guides/edf/edf-operator-guide#14-set-up-your-own-monitoring-and-alerts). +::: diff --git a/docs/contracts/delegation-factory.md b/docs/contracts/delegation-factory.md new file mode 100644 index 000000000..440bd92fd --- /dev/null +++ b/docs/contracts/delegation-factory.md @@ -0,0 +1,54 @@ +# DelegationFactory + +- [Source Code](https://github.com/lidofinance/execution-delegation-framework/blob/main/src/DelegationFactory.sol) +- \[[proposed](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/10)\] [Deployed Contract](https://etherscan.io/address/0xD990770eB2B4b6062EDdB06892fF179C693b46e6) + +`DelegationFactory` deploys [`DelegationContract`](/contracts/delegation-contract) instances for the [Execution Delegation Framework (EDF)](/guides/edf/edf-operator-guide). Anyone can call `deploy()`. The new contract's owner and cooldown cannot be changed after deployment; its initial delegate can later be rotated or revoked. + +Only contracts deployed from this factory are accepted for Lido Oracle and Deposit Security Committee seats. Factory addresses per network are listed on the [deployed contracts](/deployed-contracts/#execution-delegation-framework) page. + +## Methods + +### deploy() + +Deploys a new `DelegationContract`. + +```solidity +function deploy(address owner, address delegate, uint256 cooldown) external returns (address instance); +``` + +:::note +Reverts if any of the following is true: + +- `owner` is zero address; +- `delegate` is equal to `owner`. +::: + +#### Parameters + +| Name | Type | Description | +| ---------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `owner` | `address` | Owner of the new contract. Cannot be changed later. | +| `delegate` | `address` | Initial delegate, active immediately. Zero address deploys the contract without a delegate. | +| `cooldown` | `uint256` | Seconds between a nomination and the moment the new delegate becomes active. Cannot be changed later. The [custody policy](/guides/edf/key-custody-policy-for-edf-operators#3-owner-key-custody) requires at least 172800 (48 hours). | + +#### Returns + +| Name | Type | Description | +| ---------- | --------- | --------------------------------------- | +| `instance` | `address` | Address of the new `DelegationContract` | + +## Events + +### DelegationContractDeployed() + +Emitted for each deployed `DelegationContract`. + +```solidity +event DelegationContractDeployed( + address indexed instance, + address indexed owner, + address indexed delegate, + uint256 cooldown +); +``` diff --git a/docs/contracts/deposit-security-module.md b/docs/contracts/deposit-security-module.md index 383cc2711..dbe6ca83c 100644 --- a/docs/contracts/deposit-security-module.md +++ b/docs/contracts/deposit-security-module.md @@ -2,6 +2,7 @@ - [Source Code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/DepositSecurityModule.sol) - [Deployed Contract](https://etherscan.io/address/0xF573E9E3de1f86B085417ab294f56E7920B4e9Be) +- \[[proposed](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/25)\] [Deployed Contract (DSM v5)](https://etherscan.io/address/0x39BB5d491e98A44D1bfe8047A737a81E296a63E0) Due to front-running vulnerability, Lido contributors [proposed](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-5.md) to establish the Deposit Security Committee dedicated to ensuring the safety of deposits on the Beacon chain: diff --git a/docs/guides/curated-module/exits/tooling-setup.md b/docs/guides/curated-module/exits/tooling-setup.md index 58d389db4..8bdb8bbf4 100644 --- a/docs/guides/curated-module/exits/tooling-setup.md +++ b/docs/guides/curated-module/exits/tooling-setup.md @@ -39,12 +39,16 @@ ID of the [StakingRouter](https://github.com/lidofinance/core/blob/master/contra Currently, it has only one module ([NodeOperatorsRegistry](https://github.com/lidofinance/core/blob/master/contracts/0.4.24/nos/NodeOperatorsRegistry.sol)), it's id is `1`. ### Oracle Allowlist -The oracle members are retrievable from the HashConsensus (for the Validator Exit Bus Oracle ) contract on-chain, directly from the contract using Etherscan. + +The oracle members are retrievable from the HashConsensus (for the Validator Exit Bus Oracle) contract on-chain, directly from the contract using Etherscan. + | network | Contract Call | | -------- | ------------- | | Mainnet | [getMembers()](https://etherscan.io/address/0x7FaDB6358950c5fAA66Cb5EB8eE5147De3df355a#readContract#F16) | | Hoodi | [getMembers()](https://hoodi.etherscan.io/address/0x30308CD8844fb2DB3ec4D056F1d475a802DCA07c#readContract#F16) | +Under the [Execution Delegation Framework (EDF)](/guides/edf/edf-operator-guide), `getMembers()` returns `DelegationContract` addresses. `ORACLE_ADDRESSES_ALLOWLIST` must contain the members' **delegate EOAs** instead. See [ORACLE_ADDRESSES_ALLOWLIST](/guides/validator-ejector-guide#oracle_addresses_allowlist) in the Ejector guide. + ## Example Infra Setup Lido DevOps team prepared an easy way to get the recommended tooling and its dependencies up and running using [Ansible](https://github.com/ansible/ansible). This is a great way to get familiar with the new tooling. This is an example implementation, and still requires security and hardening by the NO; it can be found on [GitHub](https://github.com/lidofinance/node-operators-setup). diff --git a/docs/guides/deposit-security-manual.md b/docs/guides/deposit-security-manual.md index 397bc60da..05e652cb3 100644 --- a/docs/guides/deposit-security-manual.md +++ b/docs/guides/deposit-security-manual.md @@ -30,11 +30,15 @@ To make a deposit, we propose to collect a quorum of 4/6 of the signatures of th The committee consists of five node operators and the Lido dev team. The current list of guardians and their addresses is published on the [Lido Council Daemon](/holders/lido-council-daemon#mainnet-members) page. In the future, we want to bring as many node operators as possible into the mix, so the expectation will be that while the 6 guardians start the rest of the node operators can also participate via testnet and gradually get pulled into mainnet. -After the [LIP-37](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746) vote, a guardian seat is held by the member's `DelegationContract` under the [Execution Delegation Framework (EDF)](/guides/edf/edf-operator-guide), not by an EOA. The member's hot key becomes the delegate of that contract and can be rotated or revoked by the member without a governance vote. +A guardian seat is held by the member's `DelegationContract` under the [Execution Delegation Framework (EDF)](/guides/edf/edf-operator-guide), not by an EOA. The member's hot key becomes the delegate of that contract and can be rotated or revoked by the member without a governance vote. ### Members responsibilities -Each member must prepare a hot key to sign the pair `(depositRoot, keysOpIndex)`. The address added to the smart contract is the member's `DelegationContract` (before the LIP-37 vote: the member's EOA), and the hot key is its delegate. The `DepositSecurityModule` verifies guardian signatures through ERC-1271, so a signature is valid only while the signing key is the active delegate of the contract. Also, members have to run `DSC Daemon` that monitors the validators’ public keys in the `DepositContract` and in all Staking Modules. The daemon must have access to the delegate’s private key to be able to perform ECDSA signing. See the [EDF Operator Guide](/guides/edf/edf-operator-guide) for the setup. +Each member must prepare a hot key to sign the deposit attestation message. In `DepositSecurityModule` v5 the signed message is `(prefix, guardian, blockNumber, blockHash, depositRoot, stakingModuleId, nonce)`, where `guardian` is the member's `DelegationContract` address. The pause and unvet messages include the guardian address in the same way. The address added to the smart contract is the member's `DelegationContract`, and the hot key is its delegate. The `DepositSecurityModule` verifies guardian signatures through ERC-1271, so a signature is valid only while the signing key is the active delegate of the contract. + +`pauseDeposits` and `unvetSigningKeys` can also be called directly by a guardian; then the signature argument is ignored. Under EDF the delegate calls `execute()` on its `DelegationContract`, so the contract is the sender. The council daemon uses this path to pause deposits and unvet keys, and also broadcasts the signed message so that anyone can relay it. + +Members also have to run `DSC Daemon` that monitors the validators’ public keys in the `DepositContract` and in all Staking Modules. The daemon must have access to the delegate’s private key to be able to perform ECDSA signing. See the [EDF Operator Guide](/guides/edf/edf-operator-guide) for the setup. ## Preparation steps diff --git a/docs/guides/edf/edf-operator-guide.md b/docs/guides/edf/edf-operator-guide.md index 379736dd0..73b8f7a53 100644 --- a/docs/guides/edf/edf-operator-guide.md +++ b/docs/guides/edf/edf-operator-guide.md @@ -6,6 +6,7 @@ Setup instructions for operators (key holders) of a Lido Oracle seat or a DSM gu - [LIP-37: Execution Delegation Framework](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-37.md) — the proposal - [execution-delegation-framework](https://github.com/lidofinance/execution-delegation-framework) — the contracts, [architecture](https://github.com/lidofinance/execution-delegation-framework/blob/main/docs/architecture.md), [usage guide](https://github.com/lidofinance/execution-delegation-framework/blob/main/docs/usage.md) +- [DelegationFactory](/contracts/delegation-factory) and [DelegationContract](/contracts/delegation-contract) — the contract reference - [EDF Operator Key Custody Policy](./key-custody-policy-for-edf-operators.md) — the rules you must follow - [EDF Rotation and Incidents](./edf-rotation-and-incidents.md) — what to do after the setup @@ -43,7 +44,7 @@ Read the [Key Custody Policy](./key-custody-policy-for-edf-operators.md) before anything. Two of its values are irreversible: - the **owner address** — your multisig (step 0.2); -- the **cooldown** — **48 hours = `172800` seconds**. +- the **cooldown** — **48 hours = `172800` seconds**, the policy minimum. ### 0.2. Prepare the owner multisig @@ -177,7 +178,13 @@ Route these to a phone. - `execute()` calls to targets your daemon never calls, or to an EOA; - non-zero `msg.value` forwarded through `execute()`; -- direct transactions from the delegate EOA that your daemon did not send. +- direct transactions from the delegate EOA that your daemon did not send; +- non-empty code on the delegate EOA (poll `eth_getCode`; see + [section 4 of the custody policy](./key-custody-policy-for-edf-operators.md#4-delegate-hot-key-custody)). + +`execute()` emits no events, so the first two alerts need trace-level monitoring: internal +transactions from a `trace_` / `debug_` RPC, or the **Internal Transactions** tab of your +`DelegationContract` on Etherscan. ### 1.5. Publish your addresses @@ -209,6 +216,19 @@ true yet, finish that step first. --- +## Minimum software versions + +Run these versions or newer. Older releases do not support EDF. + +| Component | Minimum version | +| --- | --- | +| [lido-oracle](https://github.com/lidofinance/lido-oracle/releases) | 8.1.0 | +| [lido-council-daemon](https://github.com/lidofinance/lido-council-daemon/releases) | 4.1.2 | +| [depositor-bot](https://github.com/lidofinance/depositor-bot/releases) | 5.7.0 | +| [validator-ejector](https://github.com/lidofinance/validator-ejector/releases) | 2.2.0 | + +--- + ## Part 2 — Configure the Lido Oracle > Follow **Part 2** if you run the Lido Oracle, **Part 3** if you run the Council daemon. @@ -229,9 +249,9 @@ true yet, finish that step first. MEMBER_PRIV_KEY_2=0xnewdelegatekey # new - takes over after the vote ``` -2. **Fund the delegate EOA.** Send 50% of the current balance of your old member EOA to the new - delegate EOA (the address returned by `getDelegate()`). Both keys must be able to pay for gas: the - old one until the vote, the new one after it. +2. **Fund the delegate EOA (required).** Send 50% of the current balance of your + old member EOA to the new delegate EOA (the address returned by `getDelegate()`). Both keys must + be able to pay for gas: the old one until the vote, the new one after it. 3. **Restart the oracle.** @@ -243,7 +263,12 @@ At startup: - `Delegation contract is a member, but its current delegate matches none of the configured accounts.` — fix the config. - `None of the configured accounts is an active member.` — fix the config. -- `Provided Account is not part of Oracle's members and has no submit role.` — fix the config. +- `Reporting address is not a HashConsensus member and has no submit role at this block. The member + list probably changed since the signer was resolved; it will be re-resolved on the next cycle.` — + fix the config. + +The oracle does not stop on these errors: it runs in dry mode and re-checks the config every +cycle, so no restart is needed after the fix. ### 2.3. Report your oracle setup in the operators' chat @@ -296,7 +321,7 @@ unset or wrong — fix the config. | Variable | Value | | --- | --- | - | `DELEGATION_CONTRACT_ADDRESS` | Your `DelegationContract` address. Config validation **fails at startup** if it is empty or not a valid address — even while the DSM is still on v4. | + | `DELEGATION_CONTRACT_ADDRESS` | Your `DelegationContract` address. Required. On DSM v4 an **empty value is not rejected at startup**, so check for `EDF preflight passed` in the logs (step 3.2). | | `WALLET_PRIVATE_KEY` / `WALLET_PRIVATE_KEY_FILE` | **The old key** — your existing guardian EOA. Used while the DSM is on v4. | | `WALLET_PRIVATE_KEY_2` / `WALLET_PRIVATE_KEY_2_FILE` | **The new key** — the delegate of your `DelegationContract`. | @@ -306,10 +331,10 @@ unset or wrong — fix the config. WALLET_PRIVATE_KEY_2=0xnewdelegatekey # new - takes over at DSM v5 ``` -2. **Fund the delegate EOA.** Send 50% of the current balance of your old guardian EOA to the new - delegate EOA (the address returned by `getDelegate()`). Both keys must be able to pay for gas: the - old one until DSM v5, the new one after it. Do the same on the DataBus chain (Gnosis): the delegate - EOA needs xDAI there to send Data Bus messages. +2. **Fund the delegate EOA (required).** Send 50% of the current balance of your + old guardian EOA to the new delegate EOA (the address returned by `getDelegate()`). Both keys must + be able to pay for gas: the old one until DSM v5, the new one after it. Do the same on the DataBus + chain (Gnosis): the delegate EOA needs xDAI there to send Data Bus messages. 3. **Restart the daemon.** @@ -325,11 +350,22 @@ Guardian execution mode: edf dsmVersion: 5 ``` +On DSM v4 the daemon runs in `legacy-eoa` mode and checks the EDF config once at startup: + +- `EDF preflight passed` — the contract is found, not terminated, and its delegate matches one of + the configured keys. +- `EDF setup is not ready` with a `reason` field — fix the config. The daemon keeps running on DSM + v4 anyway. + +This is the only check on DSM v4. A daemon with a wrong EDF config does not crash when the DSM +switches to v5: it logs `Guardian cycle processing error` every cycle and signs nothing. A restart +then exits with code 1 and one of the errors below. + Errors you may hit, and what they mean: | Error | Meaning | | --- | --- | -| `DELEGATION_CONTRACT_ADDRESS is required for DSM version 5` | Variable not set. | +| `DELEGATION_CONTRACT_ADDRESS is required for DSM version 5` | Variable not set, and the DSM is on v5. | | `No contract code at DELEGATION_CONTRACT_ADDRESS 0x…` | Wrong address, or wrong network. | | `DelegationContract 0x… is terminated` | Someone called `terminate()`. The seat is permanently dead. | | `DelegationContract 0x… has no active delegate` | The delegate was revoked, or never set. Expected right after an emergency revocation. | diff --git a/docs/guides/edf/edf-rotation-and-incidents.md b/docs/guides/edf/edf-rotation-and-incidents.md index 5308a9512..9f03fb2ef 100644 --- a/docs/guides/edf/edf-rotation-and-incidents.md +++ b/docs/guides/edf/edf-rotation-and-incidents.md @@ -44,7 +44,9 @@ 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`. + `ORACLE_ADDRESSES_ALLOWLIST`. Node operators must add the new delegate before `activeFrom` and + keep the old one for about 7 days after the switch (the Ejector lookback window, + `BLOCKS_PRELOAD` = 50000 blocks by default). 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 diff --git a/docs/guides/edf/key-custody-policy-for-edf-operators.md b/docs/guides/edf/key-custody-policy-for-edf-operators.md index bd0285f60..2db9063f6 100644 --- a/docs/guides/edf/key-custody-policy-for-edf-operators.md +++ b/docs/guides/edf/key-custody-policy-for-edf-operators.md @@ -72,6 +72,10 @@ Treat the owner setup as a long-lived commitment and get it right before deploym - **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**. +6. **The cooldown MUST be at least 48 hours.** + + The `cooldown` passed to `DelegationFactory.deploy()` MUST be at least **48 hours (172800 seconds)**. The cooldown is the owner's only window to react to a hostile `DelegateNominated`, and it cannot be changed after deployment. The contract does not enforce a minimum, so it is checked at admission: a `DelegationContract` with a shorter cooldown is not accepted for a seat. + --- @@ -98,6 +102,14 @@ Treat the owner setup as a long-lived commitment and get it right before deploym 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. **The delegate MUST be a plain externally owned account.** + + The delegate address MUST have empty code for the whole time it is the delegate: no smart contract, no smart-contract wallet, no EIP-7702 delegation designator. The `DelegationContract` checks the delegate's code length to choose between ECDSA recovery and an ERC-1271 call. Code on the delegate therefore changes which signatures the seat accepts, with no event and no change in `getDelegate()`. Operators SHOULD poll the delegate's code and alert when it is not empty (see §7). + +6. **The delegate key MUST never sign an EIP-7702 authorization.** + + An EIP-7702 authorization installs code on the delegate account and breaks the rule above. If such an authorization has been signed, treat it as a §6.1 event and revoke the delegate. + --- @@ -230,6 +242,10 @@ Alongside Lido’s protocol-wide monitoring, each operator SHOULD independently - Unexpected `execute()` targets, including EOA destinations - Unexpected non-zero `msg.value` forwarded through `execute()` - Transactions from the delegate EOA itself + - `execute()` emits no events, so the first two checks need trace-level monitoring (internal transactions) +- **Code on the delegate account** + - Poll the code of the delegate address (`eth_getCode` / EXTCODESIZE) and alert when it is not empty + - No event marks this change, so polling is the only detector (see §4.5) ### Emergency contact diff --git a/docs/guides/oracle-operator-manual.md b/docs/guides/oracle-operator-manual.md index a7ec5c38c..f7e83e8c9 100644 --- a/docs/guides/oracle-operator-manual.md +++ b/docs/guides/oracle-operator-manual.md @@ -13,7 +13,7 @@ Due to the lack of native communication between these two networks, Lido employs 6. [**Optional**] Add alerts to Oracle's Prometheus metrics. 7. In case of mainnet, share your address and intention to join the Oracle set with the public. You need to publish it on Twitter and also write a message with a Twitter link under the Onboarding post on [the Research forum](https://research.lido.fi/). You need to publish it on Twitter and also write a message with a twitter link under the Onboarding post on [the Research forum](https://research.lido.fi/). 8. Propose your Oracle's Ethereum address to the Lido team to vote on adding your address to the Oracle Members. -9. After the [LIP-37](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746) vote, the seat is held by a `DelegationContract` instead of an EOA: deploy it and configure the daemon as described in the [EDF Operator Guide](/guides/edf/edf-operator-guide). +9. Under the Execution Delegation Framework (EDF), the seat is held by a `DelegationContract`, not by an EOA. Deploy it and configure the daemon as described in the [EDF Operator Guide](/guides/edf/edf-operator-guide) before you propose the address in step 8. ## Intro diff --git a/docs/guides/protocol-levers.md b/docs/guides/protocol-levers.md index a61eb7a46..fe8b23a67 100644 --- a/docs/guides/protocol-levers.md +++ b/docs/guides/protocol-levers.md @@ -105,7 +105,7 @@ Key levers on [StakingRouter](/contracts/staking-router/) ([`0xFdDf38947aFB03C62 | Module registry | `addStakingModule()`, `updateStakingModule()`, `setStakingModuleStatus()` | `STAKING_MODULE_MANAGE_ROLE` | StakingRouter | Aragon Agent | Aragon Agent ([`0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c`](https://etherscan.io/address/0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c)) | | Module fees | `setStakingModuleFees()` | `STAKING_MODULE_MANAGE_ROLE` | StakingRouter | Aragon Agent | Aragon Agent ([`0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c`](https://etherscan.io/address/0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c)) | | Withdrawal credentials | `setWithdrawalCredentials()` | `MANAGE_WITHDRAWAL_CREDENTIALS_ROLE` | StakingRouter | Aragon Agent | Unassigned | -| Module unvetting | `decreaseStakingModuleVettedKeysCountByNodeOperator()` | `STAKING_MODULE_UNVETTING_ROLE` | StakingRouter | Aragon Agent | [DepositSecurityModule](/contracts/deposit-security-module/) ([`0xfFA96D84dEF2EA035c7AB153D8B991128e3d72fD`](https://etherscan.io/address/0xfFA96D84dEF2EA035c7AB153D8B991128e3d72fD)) | +| Module unvetting | `decreaseStakingModuleVettedKeysCountByNodeOperator()` | `STAKING_MODULE_UNVETTING_ROLE` | StakingRouter | Aragon Agent | \[[proposed to remove](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/25)\] [DepositSecurityModule](/contracts/deposit-security-module/) ([`0xF573E9E3de1f86B085417ab294f56E7920B4e9Be`](https://etherscan.io/address/0xF573E9E3de1f86B085417ab294f56E7920B4e9Be)), \[[proposed](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/25)\] DSM v5 ([`0x39BB5d491e98A44D1bfe8047A737a81E296a63E0`](https://etherscan.io/address/0x39BB5d491e98A44D1bfe8047A737a81E296a63E0)) | ### Active staking modules diff --git a/docs/guides/tooling.md b/docs/guides/tooling.md index 52884c909..3df7358e3 100644 --- a/docs/guides/tooling.md +++ b/docs/guides/tooling.md @@ -24,33 +24,33 @@ Oracle daemon for Lido decentralized staking service. Daemon service which loads LidoOracle events for validator exits and sends out exit messages when necessary. -- **Version**: 2.1.0 -- **Docker image**: sha256:8953a4107d99ab84ff0f2b02cb7dd13b7cd7e5a565cf04fbe36e7911df5983dc, [lidofinance/validator-ejector@sha256-8953a4107d99ab84ff0f2b02cb7dd13b7cd7e5a565cf04fbe36e7911df5983dc](https://hub.docker.com/layers/lidofinance/validator-ejector/2.1.0/images/sha256-8953a4107d99ab84ff0f2b02cb7dd13b7cd7e5a565cf04fbe36e7911df5983dc) -- **Commit hash**: [lidofinance/validator-ejector@ec0992d](https://github.com/lidofinance/validator-ejector/commit/ec0992d9b4454425470b6608336755419ddb94ca) -- **Last update date**: 26 May, 2026 -- [**Repository**](https://github.com/lidofinance/validator-ejector/tree/2.1.0) +- **Version**: 2.2.0 +- **Docker image**: sha256:119841189487da4e049270abf6ed01b82a42c739878894528d4fefb600a471f5, [lidofinance/validator-ejector@sha256-119841189487da4e049270abf6ed01b82a42c739878894528d4fefb600a471f5](https://hub.docker.com/layers/lidofinance/validator-ejector/2.2.0/images/sha256-119841189487da4e049270abf6ed01b82a42c739878894528d4fefb600a471f5) +- **Commit hash**: [lidofinance/validator-ejector@debecf4](https://github.com/lidofinance/validator-ejector/commit/debecf42ac9f5ce7ccbd415274c8ef40f884b124) +- **Last update date**: 21 August, 2026 +- [**Repository**](https://github.com/lidofinance/validator-ejector/tree/2.2.0) - [**Documentation**](/guides/validator-ejector-guide) ## Council daemon The Lido Council Daemon monitors deposit contract keys. -- **Version**: 4.0.4 -- **Docker image**: sha256:8e419905599b55cf37dc51f667468e7a24c34e7b5bade17e7f08691e98dbdb02, [lidofinance/lido-council-daemon@sha256-8e419905599b55cf37dc51f667468e7a24c34e7b5bade17e7f08691e98dbdb02](https://hub.docker.com/layers/lidofinance/lido-council-daemon/4.0.4/images/sha256-8e419905599b55cf37dc51f667468e7a24c34e7b5bade17e7f08691e98dbdb02) -- **Commit hash**: [lidofinance/lido-council-daemon@b02577f](https://github.com/lidofinance/lido-council-daemon/commit/b02577ff193ea8fa96f5c16025292d044ebd70f3) -- **Last update date**: 7 July, 2026 -- [**Repository**](https://github.com/lidofinance/lido-council-daemon/tree/4.0.4) +- **Version**: 4.1.2 +- **Docker image**: sha256:4c204661e0c930be50a0d42155342c2988f1b024d1d8896250197c4256347aa7, [lidofinance/lido-council-daemon@sha256-4c204661e0c930be50a0d42155342c2988f1b024d1d8896250197c4256347aa7](https://hub.docker.com/layers/lidofinance/lido-council-daemon/4.1.2/images/sha256-4c204661e0c930be50a0d42155342c2988f1b024d1d8896250197c4256347aa7) +- **Commit hash**: [lidofinance/lido-council-daemon@d3bc5e8](https://github.com/lidofinance/lido-council-daemon/commit/d3bc5e8fe968293530f3ec976c30230d98f671de) +- **Last update date**: 7 September, 2026 +- [**Repository**](https://github.com/lidofinance/lido-council-daemon/tree/4.1.2) - [**Documentation**](/guides/deposit-security-manual) ## Depositor Bot Bot that submits deposit transactions to the Lido protocol once the Deposit Security Committee quorum is reached. -- **Version**: 5.6.0 -- **Docker image**: sha256:a8fc015713cf4680bf2d2692de7a295ac99d00d29bb154860c285a44e63e0c32, [lidofinance/depositor-bot@sha256-a8fc015713cf4680bf2d2692de7a295ac99d00d29bb154860c285a44e63e0c32](https://hub.docker.com/layers/lidofinance/depositor-bot/5.6.0/images/sha256-a8fc015713cf4680bf2d2692de7a295ac99d00d29bb154860c285a44e63e0c32) -- **Commit hash**: [lidofinance/depositor-bot@ccb788e](https://github.com/lidofinance/depositor-bot/commit/ccb788e041cf7a95ff5f9a1894bb67fd5393124c) -- **Last update date**: 24 July, 2026 -- [**Repository**](https://github.com/lidofinance/depositor-bot/tree/5.6.0) +- **Version**: 5.7.0 +- **Docker image**: sha256:5289b2a070190adcdf70d7cc54885235faaa8ac015c4e57977ebfc4160ee59ae, [lidofinance/depositor-bot@sha256-5289b2a070190adcdf70d7cc54885235faaa8ac015c4e57977ebfc4160ee59ae](https://hub.docker.com/layers/lidofinance/depositor-bot/5.7.0/images/sha256-5289b2a070190adcdf70d7cc54885235faaa8ac015c4e57977ebfc4160ee59ae) +- **Commit hash**: [lidofinance/depositor-bot@b5ea173](https://github.com/lidofinance/depositor-bot/commit/b5ea173eb86c27bf164c5f6ca8bc862be869736e) +- **Last update date**: 8 September, 2026 +- [**Repository**](https://github.com/lidofinance/depositor-bot/tree/5.7.0) - [**Documentation**](/guides/depositor-bot) ## Reward Distribution Bot diff --git a/docs/guides/validator-ejector-guide.md b/docs/guides/validator-ejector-guide.md index 61aecbcc5..6cb544c75 100644 --- a/docs/guides/validator-ejector-guide.md +++ b/docs/guides/validator-ejector-guide.md @@ -196,7 +196,11 @@ On the endpoint, JSON will be POSTed with the following structure: JSON array of Lido Oracle addresses, from which only report transactions will be accepted. -You can get a list from Etherscan on [Hoodi](https://hoodi.etherscan.io/address/0x32EC59a78abaca3f91527aeB2008925D5AaC1eFC#readContract#F16) or [Mainnet](https://etherscan.io/address/0xD624B08C83bAECF0807Dd2c6880C3154a5F0B288#readContract#F16) +Every oracle seat is held by a `DelegationContract` under the [Execution Delegation Framework (EDF)](/guides/edf/edf-operator-guide). The member's **delegate EOA** sends each report through `DelegationContract.execute(address,bytes)`, and the Ejector verifies the report by recovering the signer of that transaction. So: + +- The allowlist must contain the **delegate EOAs** of the oracle members, not the `DelegationContract` addresses that `getMembers()` on `HashConsensus` returns ([Hoodi](https://hoodi.etherscan.io/address/0x30308CD8844fb2DB3ec4D056F1d475a802DCA07c#readContract#F16), [Mainnet](https://etherscan.io/address/0x7FaDB6358950c5fAA66Cb5EB8eE5147De3df355a#readContract#F16)). Take each `DelegationContract` from the [Lido Oracle members page](/holders/lido-oracle) and read its `getDelegate()` on Etherscan, or use the delegate addresses that oracle operators publish in the [LIP-37 forum thread](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746) and in rotation announcements (see [EDF Rotation and Incidents](/guides/edf/edf-rotation-and-incidents)). +- After a delegate rotation, keep the previous delegate in the allowlist until its reports leave the lookback window: about 7 days at the default `BLOCKS_PRELOAD` of 50000 blocks. +- Use validator-ejector [2.2.0](https://github.com/lidofinance/validator-ejector/releases/tag/2.2.0) or newer: it unwraps `execute(address,bytes)`. Older releases reject every report sent through a `DelegationContract`. Format: diff --git a/sidebars.js b/sidebars.js index 056759bf2..eb3afc57e 100644 --- a/sidebars.js +++ b/sidebars.js @@ -148,6 +148,8 @@ module.exports = { 'contracts/wsteth', 'contracts/wsteth-staker', 'contracts/deposit-security-module', + 'contracts/delegation-factory', + 'contracts/delegation-contract', 'contracts/data-bus', 'contracts/burner', 'contracts/lido-execution-layer-rewards-vault',