For the complete documentation index, see llms.txt. This page is also available as Markdown.

Lido Withdrawal Queue

A plain adapter that reports the native ETH owed to an account from open requests in Lido's WithdrawalQueueERC721.

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.


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

Flowchart of the Lido Withdrawal Queue balance adapter: read calls derive position legs into PositionBalance entries.
Lido Withdrawal Queue adapter — how the underlying balances reported to the NAV Calculator are derived.

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 — 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:

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

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]).

Last updated