Skip to content
Merged
209 changes: 209 additions & 0 deletions docs/contracts/delegation-contract.md
Original file line number Diff line number Diff line change
@@ -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.
Comment thread
chasingrainbows marked this conversation as resolved.

```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).
:::
54 changes: 54 additions & 0 deletions docs/contracts/delegation-factory.md
Original file line number Diff line number Diff line change
@@ -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
);
```
1 change: 1 addition & 0 deletions docs/contracts/deposit-security-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
6 changes: 5 additions & 1 deletion docs/guides/curated-module/exits/tooling-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
8 changes: 6 additions & 2 deletions docs/guides/deposit-security-manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading
Loading