Skip to main content

ConsolidationGateway

ConsolidationGateway is the single entry point for EIP-7251 validator consolidation requests in Lido Core. It proxies consolidation requests to the WithdrawalVault, checking permissions, verifying target validators' withdrawal credentials, applying rate limits, and refunding any excess fee.

What is ConsolidationGateway​

A consolidation is an EIP-7251 operation that merges a source validator's balance into a target 0x02 (compounding) validator. A consolidation request must be signed by the source validator's withdrawal address, and since Lido Core validators use WithdrawalVault withdrawal credentials, requests to the EIP-7251 system contract are submitted from the WithdrawalVault. The vault, in turn, accepts consolidation requests only from ConsolidationGateway.

Placing this logic in a dedicated contract rather than in the WithdrawalVault itself allows the consolidation flow to be paused independently in an emergency, without affecting protocol withdrawals or triggerable exits.

The contract inherits OpenZeppelin AccessControlEnumerable, PausableUntil, and CLProofVerifier. It is not deployed behind a proxy.

note

ConsolidationGateway serves the Lido Core consolidation flow. For consolidations into stVaults, see ValidatorConsolidationRequests.

Consolidation flow in Lido Core​

The gateway is the last permissioned step of the consolidation pipeline introduced in LIP-35 to migrate stake between staking modules (initially, from Curated Module v1 to Curated Module v2):

  1. A node operator obtains permission to migrate stake via an EasyTrack motion that allowlists a (source operator → target operator) pair with a designated Consolidation Manager address on the ConsolidationMigrator.
  2. The Consolidation Manager submits validator key indices to the Migrator, which validates them, resolves them to public keys via the staking modules, and publishes the batch to the ConsolidationBus.
  3. After the bus's execution delay has elapsed, a permissionless executor calls ConsolidationBus.executeConsolidation, supplying the published batch together with withdrawal credentials proofs for the target validators and the required fee. The bus forwards the batch to the gateway.
  4. The gateway validates the request (see Request processing) and forwards it, together with the exact required fee, to WithdrawalVault.addConsolidationRequests, which submits one EIP-7251 request per (source, target) pair to the system contract.

Request processing​

addConsolidationRequests accepts consolidation requests grouped by target validator: each group contains the source validators' public keys and a ValidatorWitness proving the shared target validator's withdrawal credentials. Before forwarding anything to the WithdrawalVault, the gateway enforces the following:

  1. Access. The caller holds ADD_CONSOLIDATION_REQUEST_ROLE (intended for the ConsolidationBus), and the contract is not paused.
  2. Well-formedness. A non-zero fee is attached, the batch is not empty, and every group contains at least one source key.
  3. Protocol state. Deposits are not paused on the DepositSecurityModule, and Lido.canDeposit() returns true (i.e., the protocol is not stopped and bunker mode is not active). See Protocol state checks.
  4. Target withdrawal credentials. Every target validator's withdrawal credentials are proven on-chain to match the Lido 0x02 withdrawal credentials. See Target validator verification.
  5. Rate limit. The number of individual requests in the batch (the total count of source keys) fits into the current frame's quota. See Consolidation limits.
  6. Fee. The attached ether covers requestsCount × WithdrawalVault.getConsolidationRequestFee(). Exactly this amount is forwarded to the vault, and any excess is refunded to the refundRecipient (or to the caller if the recipient is the zero address).

The entire call is wrapped in a balance-preservation check (the preservesEthBalance modifier): the gateway never accumulates ether — everything it receives is either forwarded to the WithdrawalVault or refunded within the same transaction.

Protocol state checks​

Consolidation requests are blocked while DepositSecurityModule deposits are paused. If the DSM has paused deposits, some recently deposited validators may not belong to Lido and can therefore have non-Lido withdrawal credentials; blocking consolidations in this state acts as an additional safety check on top of the withdrawal credentials proof verification. Combined with the execution delay on the ConsolidationBus, this gives honest DSM guardians time to pause deposits — and thereby block pending consolidations — if keys with invalid withdrawal credentials were deposited.

For the same reason, requests are also blocked while the protocol is not operating normally: if Lido is stopped or bunker mode is active (Lido.canDeposit() returns false), the call reverts.

Target validator verification​

A consolidation credits the source validator's balance to the target validator, so the gateway must ensure the target is controlled by the protocol. The expected withdrawal credentials are derived on the fly as the 0x02 prefix joined with the WithdrawalVault address:

withdrawalCredentials = 0x02 | 11 zero bytes | 20-byte WithdrawalVault address

For each group, the target validator's actual withdrawal credentials are verified against this value using an on-chain Merkle proof of the validator container in the Consensus Layer state, anchored to a beacon block root obtained via EIP-4788 (the CLProofVerifier base contract).

Source validators are not verified by the gateway: an EIP-7251 consolidation request is only actionable by the Consensus Layer if the source validator actually has the protocol's withdrawal credentials, so an invalid source key results in a no-op request. Source keys are additionally validated upstream by the ConsolidationMigrator and ConsolidationBus.

Consolidation limits​

To prevent overload, the gateway rate-limits the number of consolidation requests using a replenishing quota (the same RateLimit mechanism as the TriggerableWithdrawalsGateway exit limits):

  • maxConsolidationRequestsLimit — the quota ceiling;
  • consolidationsPerFrame — how many requests are restored to the quota per frame;
  • frameDurationInSec — the frame duration in seconds.

Each processed request consumes one unit of the quota, and the quota is restored at a rate of consolidationsPerFrame per frame up to the ceiling. If a batch does not fit into the remaining quota, the whole call reverts with ConsolidationRequestsLimitExceeded. Setting maxConsolidationRequestsLimit to zero disables limiting entirely.

The limit parameters are set at deployment and can be updated via setConsolidationRequestLimit; the current state is readable via getConsolidationRequestLimitFullInfo.

Roles​

Access to lever methods is restricted using the functionality of the OpenZeppelin AccessControlEnumerable contract:

  • DEFAULT_ADMIN_ROLE — manages role assignments; held by the Lido DAO Aragon Agent;
  • ADD_CONSOLIDATION_REQUEST_ROLE — allows submitting consolidation requests; intended for the ConsolidationBus;
  • EXIT_LIMIT_MANAGER_ROLE — allows updating the consolidation limits;
  • PAUSE_ROLE / RESUME_ROLE — allow pausing and resuming the contract (see PausableUntil).

View methods​

getConsolidationRequestLimitFullInfo​

Returns the full state of the consolidation limits.

function getConsolidationRequestLimitFullInfo()
external
view
returns (
uint256 maxConsolidationRequestsLimit,
uint256 consolidationsPerFrame,
uint256 frameDurationInSec,
uint256 prevConsolidationRequestsLimit,
uint256 currentConsolidationRequestsLimit
);

Returns:

NameTypeDescription
maxConsolidationRequestsLimituint256maximum number of consolidation requests (the quota ceiling)
consolidationsPerFrameuint256number of requests restored to the quota per frame
frameDurationInSecuint256frame duration in seconds
prevConsolidationRequestsLimituint256quota left after the previous requests
currentConsolidationRequestsLimituint256current quota; type(uint256).max if the limit is not set

Write methods​

addConsolidationRequests​

Submits grouped consolidation requests to the WithdrawalVault. Each group represents multiple source validators consolidating into a single target validator, whose withdrawal credentials are verified on-chain. Any excess ether beyond the exact required fee is refunded to refundRecipient. See Request processing for the detailed flow.

Restricted to the ADD_CONSOLIDATION_REQUEST_ROLE role, intended for the ConsolidationBus.

function addConsolidationRequests(
ConsolidationWitnessGroup[] calldata groups,
address refundRecipient
) external payable;

Parameters:

NameTypeDescription
groupsConsolidationWitnessGroup[]consolidation groups, each containing source public keys and a target validator witness with a withdrawal credentials proof
refundRecipientaddressaddress to receive any excess ether sent for fees; if zero, the excess is refunded to the caller

Reverts:

  • if the caller does not have the ADD_CONSOLIDATION_REQUEST_ROLE or the contract is paused;
  • with ZeroArgument if no ether is attached or groups is empty, and with EmptyGroup if any group has no source keys;
  • with DSMDepositsPaused or LidoDepositsPaused if the protocol state checks fail;
  • if any target validator's withdrawal credentials proof fails verification;
  • with ConsolidationRequestsLimitExceeded if the batch does not fit into the current frame's quota;
  • with InsufficientFee if the attached ether does not cover the total fee, and with FeeRefundFailed if the excess fee refund fails.

setConsolidationRequestLimit​

Updates the consolidation limits: the quota ceiling and the pace at which the quota is restored. Restricted to the EXIT_LIMIT_MANAGER_ROLE role.

function setConsolidationRequestLimit(
uint256 maxConsolidationRequestsLimit,
uint256 consolidationsPerFrame,
uint256 frameDurationInSec
) external;

Parameters:

NameTypeDescription
maxConsolidationRequestsLimituint256maximum number of consolidation requests; zero disables limiting
consolidationsPerFrameuint256number of requests restored to the quota per frame
frameDurationInSecuint256frame duration in seconds

pauseFor​

Pauses the contract for the specified duration, blocking new consolidation requests. Restricted to the PAUSE_ROLE role.

function pauseFor(uint256 _duration) external;

Parameters:

NameTypeDescription
_durationuint256pause duration in seconds (use PAUSE_INFINITELY for unlimited)

pauseUntil​

Pauses the contract until the specified timestamp (inclusive). Restricted to the PAUSE_ROLE role.

function pauseUntil(uint256 _pauseUntilInclusive) external;

Parameters:

NameTypeDescription
_pauseUntilInclusiveuint256the last second to pause until, inclusive

resume​

Resumes the contract. Restricted to the RESUME_ROLE role.

function resume() external;

Events​

ConsolidationRequestsLimitSet​

Emitted when the consolidation limits are set or updated.

event ConsolidationRequestsLimitSet(
uint256 maxConsolidationRequestsLimit,
uint256 consolidationsPerFrame,
uint256 frameDurationInSec
);