> 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/stader-withdrawal-queue.md).

# Stader Withdrawal Queue

A plain adapter that reports the ETH owed to an account from open requests in Stader's `UserWithdrawalManager`. When a user requests an unstake, ETHx is burned and a sequential request id is recorded; until claimed, the value lives as native ETH owed by the manager (its amount fixed at finalization). The position is denominated in the configured underlying (the native-ETH sentinel / WETH on mainnet), not ETHx.

The account's open requests are enumerable **on-chain** — `getRequestIdsByUser(account)` returns the live set — so no assistant or cache is needed.

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

***

## Positions returned

One `Supplied` leg per open request, all sharing one static positionId. A Stader request is binary — fully claimable OR fully locked — so each open request emits exactly one leg:

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

The payout token (ETH/WETH) is carried in `balanceAsset`; `isLocked` is set from that request's state. The per-request sequential id rides in `positionInstanceId` (= `bytes32(requestId)`), not in the label. A zero-amount leg is omitted; the position is dropped entirely if that asset 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/aYppwE8UQOFhowQANFXI" alt="Flowchart of the Stader Withdrawal Queue balance adapter: read calls derive position legs into PositionBalance entries."><figcaption><p>Stader Withdrawal Queue adapter — how the underlying balances reported to the NAV Calculator are derived.</p></figcaption></figure>

The adapter reads the account's live request ids in one call, `getRequestIdsByUser(account)`, and values each with `userWithdrawRequests(id)`:

```
ids = getRequestIdsByUser(account)                    // live set only (protocol-bounded); empty ⇒ no positions
for requestId in ids:
    (_, ethXAmount, ethExpected, ethFinalized, _) = userWithdrawRequests(requestId)
    if ethFinalized > 0: amount = ethFinalized;                          isLocked = false   // exact, withdrawable now
    else:                amount = min(ethExpected, previewWithdraw(ethXAmount)); isLocked = true    // live-rate cap
    emit leg { amount, isLocked, positionInstanceId = bytes32(requestId) }
```

Stader removes an id from that array on claim (swap-and-pop in `deleteRequestId`), so `getRequestIdsByUser` holds only un-claimed requests — no per-item liveness filter is needed. A request is claimable iff `ethFinalized > 0`; those use `ethFinalized` (exact) and emit a leg with `isLocked=false`. A **pending** request is valued **conservatively** as `min(ethExpected, STAKE_POOL_MANAGER.previewWithdraw(ethXAmount))` and emitted with `isLocked=true`: `ethExpected` is frozen at request time, so a later ETHx/ETH rate drop (slashing) would overstate it until finalization — the live re-conversion of the request's burned ETHx caps that. The live read **fails open** to `ethExpected` if it reverts (never worse than the prior estimate). This mirrors the Lido adapter's capped-owed valuation.

There is **no adapter-level cap** on the read: the full request list is enumerated (Stader itself bounds it at `maxNonRedeemedUserRequestCount`, 1000 on mainnet). Reading the whole list is the honest, over-report-safe answer — a skip-to-0 guard would silently zero a **real, claimable** balance, which is the wrong direction. The only residual is gas: a griefed account (see below) makes **its own** NAV read expensive, which under a constrained gas budget can OOG the aggregate `getAccountNav` for **that account only**. That failure is liveness-only and safe — NAV is an off-chain `eth_call` oracle with no atomic on-chain consumer, so a reverting read just means the shares layer doesn't process that account, never that it consumes a wrong value, and no other account is affected.

{% hint style="info" %}
**Third-party injection is self-defeating.** `requestWithdraw(ethXAmount, _owner)` pulls the *caller's* ETHx but records the request under an arbitrary `_owner`, claimable only by `_owner`. So anyone can plant a request owned by this account — but they burn their own ETHx to do it and cannot recover it, so the injected value is **real, claimable, and self-funded** (equivalent to an airdrop, which the wallet ERC-20 adapter likewise reports). The only residual is the gas cost of reading a spammed list, bounded by Stader's own 1000-request cap and confined to that account's own NAV read (see the [griefing note](https://github.com/karpatkey/onchain-accounting/blob/main/docs/operations/stader-withdrawal-queue-griefing.md)).
{% endhint %}

***

## Identity

* **positionId:** `abi.encode(userWithdrawalManager)` (static; the Stader `UserWithdrawalManager` address)
* **positionKind:** `Supplied`
* **positionInstanceId:** `bytes32(requestId)` (the per-request sequential id; ephemeral — the storage entry is deleted on claim)
* **Labels:** the adapter implements `positionLabels(positionId)`, returning `["Withdrawal Queue", "ETHx"]` (surfaced only on the verbose read path; full breadcrumb = `["Stader", "Withdrawal Queue", "ETHx"]`). 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 payout token (ETH/WETH) 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 owed amount; read `isLocked` per leg for the claimable-vs-pending split.

***

## Constructor

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

| Parameter                | Description                                                                  |
| ------------------------ | ---------------------------------------------------------------------------- |
| `userWithdrawalManager_` | Stader `UserWithdrawalManager` contract. Must be a contract                  |
| `underlyingToken_`       | Asset positions are denominated in (native-ETH sentinel / WETH on mainnet)   |
| `navCalculator_`         | NAVCalculator address, used for asset-registry filtering. Must be a contract |

The `StaderStakePoolsManager` — whose `previewWithdraw` provides the live-rate cap for pending requests — is **derived on-chain at construction** from `userWithdrawalManager.staderConfig().getStakePoolManager()`, so there is no extra constructor argument.

***

## 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/stader-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.
