> For the complete documentation index, see [llms.txt](https://docs.kpk.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kpk.io/funds/infrastructure/onchain-accounting/balance-adapters/lido-withdrawal-queue.md).

# Lido Withdrawal Queue

A plain adapter that reports the native ETH owed to an account from open requests in Lido's `WithdrawalQueueERC721`. When a user requests an unstake, stETH is locked and an ERC-721 request id is minted; the claim pays out native ETH, so the position is denominated in ETH (priced via the ETH feed, not stETH). While queued this value is invisible to wallet and receipt-token adapters.

* **Type:** Plain adapter
* **protocolSubId:** `keccak256("lido-withdrawal")`
* **Source:** [`LidoWithdrawalQueueBalanceAdapter.sol`](https://github.com/karpatkey/onchain-accounting/blob/main/src/balances/BalanceAdapters/LidoWithdrawalQueueBalanceAdapter.sol)

***

## Positions returned

One `Supplied` leg per open (un-claimed) request, all sharing one static positionId:

| Leg                   | PositionKind | isDebt  | isLocked | Description                                                |
| --------------------- | ------------ | ------- | -------- | ---------------------------------------------------------- |
| Per finalized request | `Supplied`   | `false` | `false`  | Native ETH from a finalized request, withdrawable now      |
| Per pending request   | `Supplied`   | `false` | `true`   | Native ETH from a not-yet-finalized request, still pending |

Each request becomes its own leg, denominated in native ETH (carried in `balanceAsset`), with `isLocked` set from that request's state (finalized → `false`, pending → `true`). The per-request ERC-721 id rides in `positionInstanceId` (= `bytes32(requestId)`), not in the label. A zero-amount leg is omitted; the position is dropped entirely if the underlying (native ETH) is not registered in NAVCalculator, if there are no open requests, or if filtered out by `assetFilter`. The balance methods (`getAdapterBalances`) still report the summed total.

***

## Balance calculation

<figure><img src="/files/phDiu7ANg9me5FpeZRzc" alt="Flowchart of the Lido Withdrawal Queue balance adapter: read calls derive position legs into PositionBalance entries."><figcaption><p>Lido Withdrawal Queue adapter — how the underlying balances reported to the NAV Calculator are derived.</p></figcaption></figure>

The adapter reads `balanceOf(account)` first (the count of open request NFTs) as an O(1) short-circuit for the common zero case. If it is non-zero it reads and sums the **full** request set — there is **no adapter-level cap**. A cap that skipped a large account to 0 would silently under-report a real, claimable balance; reading it fully is always honest. The only residual is gas: a third party can inflate the list (an arbitrary `_owner` on `requestWithdrawals`, or transferring withdrawal NFTs in), but every such request is real, claimable ETH donated to the account — so at worst a griefed list makes **that account's own** NAV read expensive. If it grows past what the read can forward, the read **reverts** (`AdapterGasExhausted`) for that account rather than quietly omitting the leg — see [a starved read is not an empty read](/funds/infrastructure/onchain-accounting/balance-adapters.md#how-an-adapter-is-built) — and the failure is retryable with more gas. Either way it is never a mis-valuation, and never affects any other account. It emits one leg per un-claimed request:

```
for each request (skipping isClaimed):
    if isFinalized and lastCheckpoint != 0:
        amount = getClaimableEther([id], findCheckpointHints([id], 1, lastCheckpoint))[0]; isLocked = false
    else:
        amount = min(getPooledEthByShares(status.amountOfShares), status.amountOfStETH); isLocked = true
    emit leg { amount, isLocked, positionInstanceId = bytes32(id) }
```

A request is finalized iff `isFinalized && lastCheckpoint != 0`; those use Lido's exact claimable ETH and emit a leg with `isLocked=false`. Not-yet-finalized requests report the **conservative** `min(getPooledEthByShares(amountOfShares), amountOfStETH)` — `amountOfStETH` is snapshotted at request time, but a Lido **negative rebase** before finalization caps the eventual payout at the lower post-rebase share value, so the live share value is taken when it is smaller (in the normal case the two are equal and `amountOfStETH` is used). This share read is fully fail-open — if it reverts the adapter falls back to `amountOfStETH` (the prior behavior). Claimable lookups are done one request at a time because `findCheckpointHints` requires an ascending-sorted id array.

***

## Identity

* **positionId:** `abi.encode(withdrawalQueue)` (static; the Lido `WithdrawalQueueERC721` address)
* **positionKind:** `Supplied`
* **positionInstanceId:** `bytes32(requestId)` (the per-request ERC-721 id; ephemeral — the NFT is burned on claim)
* **Labels:** the adapter implements `positionLabels(positionId)`, returning `["Withdrawal Queue", "stETH"]` (surfaced only on the verbose read path; full breadcrumb = `[protocolName, ...labels]` = `["Lido", "Withdrawal Queue", "stETH"]`). The request id is **not** in the label — it rides in `positionInstanceId`.

Every per-request leg shares this single static positionId; legs of the same lock state are told apart by `positionInstanceId`. The owed ETH rides in `balanceAsset`. The reconciliation key is `keccak256(abi.encode(chainId, protocolSubId, positionId, balanceAsset.asset, positionKind))` — it **excludes** both `isLocked` and `positionInstanceId`, so all of an account's open requests (claimable and pending alike) reconcile into **one coordinate** and sum to the full ETH owed; read `isLocked` per leg for the claimable-vs-pending split.

***

## Constructor

```solidity
constructor(address withdrawalQueue_, address underlyingToken_, address navCalculator_)
```

| Parameter          | Description                                                                                                 |
| ------------------ | ----------------------------------------------------------------------------------------------------------- |
| `withdrawalQueue_` | Lido `WithdrawalQueueERC721` contract. Must be a contract                                                   |
| `underlyingToken_` | Asset positions are denominated in — what Lido pays on claim (native ETH; the ERC-7528 sentinel on mainnet) |
| `navCalculator_`   | NAVCalculator address, used for asset-registry filtering. Must be a contract                                |

***

## Registration

Registered as a plain adapter with `addBalanceAdapters([adapter])` and removed with `removeBalanceAdapters([adapter])`.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.kpk.io/funds/infrastructure/onchain-accounting/balance-adapters/lido-withdrawal-queue.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
