# ADR: Withdrawal Credentials Contract
The [EIP-7685](https://eips.ethereum.org/EIPS/eip-7685) (General Purpose Execution Layer Requests) introduces the ability to expose EL requests to the consensus layer.
The relevant EIPs are:
- [EIP-6110](https://eips.ethereum.org/EIPS/eip-6110): Supply validator deposits on chain
- [EIP-7002](https://eips.ethereum.org/EIPS/eip-7002): Execution layer-triggerable withdrawals
- [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251): Increase the `MAX_EFFECTIVE_BALANCE`
To leverage the new features under the EIP-7685 framework, it's propose to define an access control system to specify which contracts can trigger specific types of requests.

The EIP-7685 assumes that the source of requests should be from the execution of transactions. More specifically, transactions which make calls to designated system contracts that store the request in account.
The implementations of EIP-7002 and EIP-7251 require that valid EL requests must be added from the withdrawal credentials address. For EIP-7002 the target address for withdrawals, i.e. the address that calls the system contract must match the 0x01 withdrawal credential recorded in the beacon state. The same logic applies to EIP-7251 requests.

Several approaches were considered:
1. Single contract with specific interfaces for each request type.
2. Single contract with generalized request processing method.
3. Request manager delegate requests to processors (using `delegatecall`).
4. Requests processors forward requests through the request router.
---
### 1. Single contract with specific interfaces for each request type.
The most strightforward approach, add a method for each request type processing, verify that the caller has permission to invoke the method, validate the request, and call the dedicated EIP contract.
```solidity
interface IWithdrawalCredentials {
function addWithdrawalRequests(
bytes calldata pubkeys,
uint256[] calldata amounts
) external payable;
function addConsolidationRequests(
bytes calldata sourcePubkeys,
bytes calldata targetPubkeys
) external payable;
/*
For future EIPs new handlers will be added.
...
*/
}
```

This approach can be illustrated with the following pseudo-code:
```solidity
// Pseudo-code. Some implementation details are skipped or simplified.
library TriggerableWithdrawals {
// EIP-7002's pre-deploy contract address.
address constant WITHDRAWAL_REQUEST = 0x0c15...;
function addWithdrawalRequests(
bytes calldata pubkeys,
uint64[] calldata amounts
) internal {
// implementation skipped for brevity...
// WITHDRAWAL_REQUEST.call{value: fee}(withdrawalRequests);
}
}
library ConsolidationRequests {
// EIP-7251's pre-deploy contract address.
address constant CONSOLIDATION_REQUEST = 0x01aB...;
function addConsolidationRequests(
bytes calldata sourcePubkeys,
bytes calldata targetPubkeys
) internal {
// implementation skipped for brevity...
// CONSOLIDATION_REQUEST.call{value: fee}(consolidationRequests);
}
}
contract WithdrawalCredentials is AccessControl {
bytes32 public constant ADD_WITHDRAWAL_REQUEST_ROLE = keccak256("ADD_WITHDRAWAL_REQUEST_ROLE");
bytes32 public constant ADD_CONSOLIDATION_REQUEST_ROLE = keccak256("ADD_CONSOLIDATION_REQUEST_ROLE");
function addWithdrawalRequests(
bytes calldata pubkeys,
uint256[] calldata amounts
) external payable onlyRole(ADD_WITHDRAWAL_REQUEST_ROLE) {
TriggerableWithdrawals.addWithdrawalRequests(pubkeys, amounts);
}
function addConsolidationRequests(
bytes calldata sourcePubkeys,
bytes calldata targetPubkeys
) external payable onlyRole(ADD_CONSOLIDATION_REQUEST_ROLE) {
ConsolidationRequests.addConsolidationRequests(sourcePubkey, targetPubkey);
}
/*
For future EIPs new handlers will be added.
...
*/
}
```
#### Pros:
1. Clear and specific interface for each request type.
2. Each request type isolated in dedicated library.
3. Each request method can include parameter validation and unique logic.
4. WithdrawalCredentials contracts (including the existing Lido WithdrawalVault and upcoming Vaults) may use only the required subset of request handlers and implement unique custom logic and permission validation.
#### Cons:
1. Requires contract upgrade if new request types need to be supported.
---
### 2. Single contract with generalized request processing method.
EIP-7002 and EIP-7251 contracts follow a similar design:
1. **Get fee:** `eipContract.staticcall('');`
2. **Add request:** `eipContract.call{value: fee}(request);`
This enables the creation of a general-purpose contract capable of processing all requests.
```solidity
interface IWithdrawalCredentials {
function addRequest(
uint256 requestType,
bytes[] requests
) external payable;
}
```
Contract verify that the caller has permission to invoke the method with specific requet type, and call the dedicated EIP contract.

This approach can be illustrated with the following pseudo-code:
```solidity
// Pseudo-code. Implementation details are skipped or simplified.
contract WithdrawalCredentials is AccessControl {
mapping(uint256 => address) public eipContracts;
function addRequests(
uint256 requestType,
bytes[] calldata requests
) external payable {
bytes32 role = keccak256(abi.encodePacked(requestType));
_checkRole(role);
address eipContract = eipContracts[requestType];
uint256 fee = _getFee(eipContract);
for (uint256 i = 0; i < requests.length; ++i) {
(bool success, ) = eipContract.call{value: fee}(requests[i]);
}
}
/*
Details skipped for breviry...
function grantRequestTypeRole(uint256 requestType, address account)
function addSupportedEipRequest(uint256 requestType, address eipContract)
...
*/
}
```
#### Pros:
1. Generalized solution; no contract updates needed for new execution layer requests.
#### Cons:
1. Unintuitive general interface; callers contracts must carefully prepare request data.
2. Cannot validate request data structure or process any request specific logic.
3. Assumes future EIPs will follow a specific design pattern.
---
### 3. Request manager delegate requests to processors (using `delegatecall`).
This approach abstracts the `WithdrawalCredentials` contract as the entry point for all request types under the EIP-7685 framework, focusing on permission checks. Request handling logic is encapsulated within dedicated request-type processors.
To decouple request processors from the request manager, `delegatecall` is employed. The request manager contract (owner of the Withdrawal Credential) uses `delegatecall` to execute request processor logic.
```solidity
interface IRequestManager {
function addRequests(uint256 requestType, bytes[] requests) external payable;
}
interface IRequestProcessor {
function processRequests(bytes[] requests) external;
}
```

This approach can be illustrated with the following pseudo-code:
```solidity
// Pseudo-code. Implementation details are skipped or simplified.
contract RequestManager is IRequestManager, AccessControl {
mapping(uint256 => address) public requestProcessors;
function addRequests(
uint256 requestType,
bytes[] calldata requests
) external payable {
bytes32 role = keccak256(abi.encodePacked(requestType));
_checkRole(role);
address processor = requestProcessors[requestType];
bytes memory data = abi.encodeWithSelector(
IRequestProcessor.processRequests.selector,
requests
);
(bool success, ) = processor.delegatecall(data);
}
/*
Details skipped for breviry...
function grantRequestTypeRole(uint256 requestType, address account)
function addSupportedEipRequest(uint256 requestType, address requestProcessor)
...
*/
}
contract WithdrawalRequestProcessor is IRequestProcessor {
function processRequests(bytes[] calldata requests) external {
// Implementation skipped for brevity...
}
}
contract ConsolidationRequestProcessor is IRequestProcessor {
function processRequests(bytes[] calldata requests) external {
// Implementation skipped for brevity...
}
}
/*
For future EIPs new processors will be added.
...
*/
```
#### Pros:
1. Generalized solution; contracts do not require updates for new execution layer requests.
2. Request processors encapsulate specific logic (data validation, event triggering).
3. Abstract approach with no assumptions about future EIP designs.
#### Cons:
1. `delegatecall` enables request processors to execute arbitrary logic on behalf of the Withdrawal Credential.
2. The general interface is unintuitive; contracts must carefully prepare request data.
3. Additional processing cost: VEB → Request Manager → Request Processor → EIP contract.
---
### 4. Requests processors forward requests through the request router.
This approach assume that request handling logic is encapsulated within dedicated request-type processors, processors serves as the entry point for all request types under the EIP-7685 framework.
The `WithdrawalCredentials` contract acts as an abstract request router, it accepts requests only from request processors and routes requests to a dedicated EIP contract.
```solidity
interface IWithdrawalRequestProcessor {
function addWithdrawalRequests(
bytes calldata pubkeys,
uint256[] calldata amounts
) external payable;
}
interface IConsolidationlRequestProcessor {
function addConsolidationRequests(
bytes calldata sourcePubkeys,
bytes calldata targetPubkeys
) external payable;
}
/*
For future EIPs new processors will be added.
...
*/
interface IRequestRouter {
function forwardRequests(
uint256 requestType,
bytes[] requests,
uint256 fee
) external payable;
}
```

This approach can be illustrated with the following pseudo-code:
```solidity
// Pseudo-code. Implementation details are skipped or simplified.
contract WithdrawalRequestProcessor {
uint256 public const REQUEST_TYPE = 1;
IRequestRouter public immutable REQUESTS_ROUTER;
constructor(address requetRouter) {
REQUESTS_ROUTER = IRequestRouter(requetRouter);
}
function addWithdrawalRequests(
bytes calldata pubkeys,
uint256[] calldata amounts
) external payable onlyRole(ADD_WITHDRAWAL_REQUEST_ROLE) {
bytes[] memory requests = _prepareRequest(pubkeys, amounts);
uint256 fee = _getWithdrawalRequestFee();
REQUESTS_ROUTER.forwardRequests{value: msg.value}(REQUEST_TYPE, requests, fee);
}
}
/*
ConsolidationRequestProcessor and other procesors skipped for brevity...
*/
contract RequestRouter is IRequestRouter {
mapping(uint256 => address) public eipContracts;
function forwardRequests(
uint256 requestType,
bytes[] calldata requests,
uint256 fee
) external payable {
bytes32 role = keccak256(abi.encodePacked(requestType));
_checkRole(role);
address eipContract = eipContracts[requestType];
for (uint256 i = 0; i < requestCount; ++i) {
(bool success, ) = eipContract.call{value: fee}(requests[i]);
require(success, "Adding request failed");
}
}
/*
Details skipped for breviry...
function grantRequestTypeRole(uint256 requestType, address account)
function addSupportedEipRequest(uint256 requestType, address eipContract)
...
*/
}
```
#### Pros:
1. Generalized solution; contracts do not require updates for new execution layer requests.
2. Request processors encapsulate specific logic (data validation, event triggering).
3. Request processors provide clear and specific interface for each request type.
#### Cons:
1. Withdrawal Credential contract assumes future EIPs will follow a specific design pattern.
2. Complicated from the DAO operations perspective, processors and the request router have their own separate permission systems, which require careful configuration.
3. Additional processing cost: VEB → Request Processor → Request Router → EIP contract.
---
## Summary
The first approach prioritizes clarity, robust validation, and a secure execution model. The design is simple and adaptable to future protocol needs, but every new request type will require a contract update.
The second approach offers a general solution but makes assumptions about the design of future EIPs. Its general interface does not accommodate request-specific logic (such as validation, events, etc.) and is less intuitive than methods dedicated to specific request types.
The third approach provides a general and flexible solution but carries significant risks, as it potentially allows the execution of any code via delegatecall on behalf of the withdrawal credentials account.
The fourth approach provides a general and flexible solution with a clear interface but assumes that the design of future EIPs will follow the same pattern. If future EIPs deviate from this pattern, the Request Router interface and all associated processors may require updates.
## Decision
After internal discussions within the ValSet team and consultations with the Vaults team, **the first approach appears more reasonable** [PR with implementation](https://github.com/lidofinance/core/pull/1018). There is a common concern that the third and fourth approaches are complicated to implement and maintain. Additionally, the second solution raises concerns due to the general interface and uncertainties about the design of future EIPs.
It is worth mentioning that the Ethereum Foundation has announced only three types of requests so far. We do not know how many request types will be introduced in the future or how many of them will be utilized by Lido.
The first approach appears more reasonable if only a few new request types are expected to be added in the future. In such cases, this might be the optimal solution due to its usability and security. While other approaches are more generalized, they introduce additional complexity, inefficiencies, and potential security vulnerabilities.
**Key Drivers for Choosing the First Approach**
1. **Simplicity and Clarity**
- Each request type has its own dedicated methods and clearly defined interface.
- Dedicated functions allow for precise parameter validation and targeted permission checks.
- It is straightforward for developers and reviewers to understand how each request is handled.
2. **Robust Validation and Security**
- Minimizes the risk of unexpected or arbitrary execution paths that more generalized approaches (e.g., `delegatecall`) could introduce.
3. **Avoiding Overengineering**
- More generalized or extensible solutions require extra complexity and cost (more contracts, more forwarding calls).
- Given the uncertainty around future request types, building a simpler, more maintainable solution is preferable.
4. **Fewer Maintenance Concerns**
- While each new request type would require a contract upgrade, the frequency of these changes is expected to be low.
- A direct interface is easier to test, audit, and maintain over time.
5. **Reduced Risk of Interface Divergence**
- Some other approaches rely on assumptions that future EIPs will follow certain patterns (e.g., fee retrieval via static calls).
- By using a specific method per request type, the solution remains stable even if future EIPs deviate from existing patterns.
Overall, **the first approach** strikes a balance between **secure design**, **usability**, and **future-proofing** without adding undue complexity.
---
## Implementation Design Details
### Fee Flow

The proposed design requires that any **EIP‑7002** or **EIP‑7251** request fee is paid by the *caller contract*. The balance of the Withdrawal Vault **must remain unchanged** to ensure accurate accounting across the protocol.
### Gas Overhead
Because the caller transfers Ether to the Withdrawal Vault on each request, the transaction incurs the extra gas costs shown below.
| Component | Gas | Notes |
|-----------|-----|-------|
| Cold‑account access | **2 600** | Applies to the first externall call |
| Warm‑account access | **100** | Applies to second and futher external call |
| Non‑zero value transfer | **6700** | Charged whenever `value > 0` |
| Withdrawal Vault code execution | **≈ 600** | Code execution + memory expansion|
<small>*sources:
[ethereum.org](https://ethereum.org/en/developers/docs/evm/opcodes/),
[evm.codes](https://www.evm.codes/?fork=cancun#f1)
</small>
**Per‑request overhead**
After the first call in a transaction (i.e. excluding the cold‑account surcharge) the overhead is
```text
100 (warm account access)
+ 6 700 (value transfer)
+ 600 (vault code execution + memory expansion)
+ 2600 (EIPs contract cold access inside vault)
≈ 10 000 gas
```
Therefore **batching is highly recommended**. Calling the Withdrawal Vault **once** with 100 requests is ≈1 000 000 gas cheaper than making 100 separate calls.
### The EIPs contracts
Both **EIP-7002: Triggerable Withdrawals** and **EIP-7251: Increase the MAX_EFFECTIVE_BALANCE** follow a minimalistic approach:
- **EIP-7002** can handle both full and partial withdrawals in a single call (`eip7002Contract.call{value: fee}(encodedPubkeyAndAmount)`), with `amount = 0` signifying a full withdrawal.
- **EIP-7251** can handle both compound and consolidation requests in a single call (`eip7251Contract.call{value: fee}(encodedSourcePubkeyAndTargetPubkey)`), where compound requests use the same source and target pubkeys.
### Proposed Interface
For clarity and a more granular permission system, the withdrawal vault contract proposes separate methods for each request type.
```solidity
interface IWithdrawalCredentials {
// ────── EIP‑7002: Triggerable Withdrawals ──────
function addWithdrawalRequests(
bytes calldata pubkeys,
uint256[] calldata amounts
) external payable;
function getWithdrawalRequestFee() external view returns (uint256);
// ────── EIP‑7251: Increase MAX_EFFECTIVE_BALANCE ──────
function addConsolidationRequests(
bytes calldata sourcePubkeys,
bytes calldata targetPubkeys
) external payable;
function getConsolidationRequestFee() external view returns (uint256);
}
```
---
We would be interested to hear your opinions and thoughts.
Thank you for your time! :heart:
## Useful links
- [PR with implementation](https://github.com/lidofinance/core/pull/1018)
- [Miro board with all schemas](https://miro.com/app/board/uXjVL-aQmEM=/)
- [Triggerable Withdrawals](https://hackmd.io/@lido/SJKVeT990)
- [EIP-7685](https://eips.ethereum.org/EIPS/eip-7685)
- [EIP-7002](https://eips.ethereum.org/EIPS/eip-7002)
- [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251)