Skip to content

[Bug]: CRITICAL: Unrestricted Validator Top-Up Allows Permissionless NOs to Drain Vault Liquidity #1718

@pk009900

Description

@pk009900

Summary

I have identified a critical logic flaw in PredepositGuarantee.sol regarding the EIP-7251 (MaxEB) top-up mechanics. The topUpExistingValidators (and proveWCActivateAndTopUpValidators) functions allow the depositor role (which is controlled by the permissionless Node Operator) to arbitrarily specify the _topUp.amount up to MAX_TOPUP_AMOUNT (2,016 ETH).

Because the StakingVault funds these top-ups directly from its availableBalance(), a malicious or greedy Node Operator can unilaterally drain the Vault's unallocated liquidity buffer.

Exploit Scenario:

  1. A permissionless NO has an ACTIVATED validator.

  2. The NO observes a large availableBalance in the StakingVault.

  3. The NO's depositor calls topUpExistingValidators with _topUp.amount equal to the Vault's balance (capped at 2016 ETH per loop).

  4. The Vault's liquidity is forcefully deposited into the Beacon Chain on the NO's validator.

Expected Behavior

In a secure, bug-free implementation, the allocation of protocol liquidity must be strictly authorized by Lido's core accounting contracts (e.g., the VaultHub or a StakingRouter), rather than being unilaterally dictated by a permissionless Node Operator.

The topUpExistingValidators function should behave according to the following constraints:

Explicit Quota Verification: When the NO's depositor calls topUpExistingValidators, the PredepositGuarantee contract must check the requested _topUp.amount against a dynamically assigned "Top-Up Quota" explicitly granted to that specific Node Operator by the Lido DAO/VaultHub. It should not rely solely on a static MAX_TOPUP_AMOUNT (2,016 ETH) check.

Staged Fund Segregation: The StakingVault should strictly segregate its accounting. If an NO is authorized for a top-up using protocol funds, those funds should be moved into a specific stagedBalance allocated strictly for that NO. The _depositToBeaconChain function should only be allowed to pull from this pre-approved stagedBalance, never directly from the unallocated availableBalance() (which must be preserved for user withdrawals).

Self-Funded Top-Ups (Optional): If a Node Operator wishes to top up their validator up to the 2,048 ETH MaxEB limit without using protocol funds, the contract should require the NO to provide the ETH themselves via msg.value during the transaction, rather than draining the StakingVault.

Summary: The contract should strictly decouple the execution of a deposit (which the NO handles) from the authorization of the funds (which the protocol must control). A top-up should only succeed if Requested Amount <= Authorized Protocol Quota + Provided msg.value.

Potential Impact

  1. Liquidity Blackhole (DoS): The vault's liquid buffer is depleted, causing incoming user withdrawals to fail until validators are forcibly ejected and exit the consensus layer queue.

  2. Yield Monopolization: A single NO can hijack protocol TVL to maximize their own effective balance and MEV opportunities, bypassing Lido's intended stake allocation algorithms.

Steps to Reproduce

Prerequisites:

-An active local fork of the Ethereum mainnet.

-The StakingVault contract has accumulated a large liquid buffer (e.g., 5,000 ETH) in its availableBalance(), intended for new node operators or user withdrawals.

-The Attacker has registered as a Node Operator in the permissionless Community Staking Module and successfully brought at least one validator to the ACTIVATED stage.

Execution Flow:

  1. Monitor Liquidity: The Attacker monitors the StakingVault contract, waiting for availableBalance() to reach a highly profitable threshold (e.g., > 2,000 ETH).

  2. Prepare the Payload: The Attacker (acting from their designated depositor address) constructs an array of ValidatorTopUp structs.

  • pubkey: The public key of the Attacker's already activated validator.

  • amount: The amount they wish to siphon from the protocol's liquidity (e.g., 2000 ether).

  1. Trigger the Exploit: The Attacker calls the topUpExistingValidators(ValidatorTopUp[] calldata _topUps) function on the PredepositGuarantee contract.

  2. Bypass Checks: * The contract checks if the caller is the authorized depositor for that specific NO (Passes).

-The contract checks if the validator is ACTIVATED (Passes).

-The contract checks if _topUp.amount <= MAX_TOPUP_AMOUNT (Passes, as 2000 ETH is less than the 2016 ETH MaxEB limit).

  1. The Drain: The PredepositGuarantee contract calls _stakingVault.depositToBeaconChain(). Because there is no quota check, the StakingVault successfully pulls 2,000 ETH directly from its availableBalance() and sends it to the Beacon Chain deposit contract, credited to the Attacker's validator.

Possible Solutions

Top-ups using protocol funds must not be at the sole discretion of the Node Operator. You must introduce a topUpQuota or an explicit allocation limit set by the VaultHub or DepositSecurityModule that caps how much of the Vault's availableBalance a specific NO is authorized to stake.

Guidelines

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions