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

ether.fi Withdrawal Queue

An assisted adapter that reports the in-flight ETH owed to an account in ether.fi's withdrawal queue, fed on-chain by the owner Safe.

An assisted adapter that reports the in-flight ETH owed to an account in ether.fi's withdrawal queue. Requesting a withdrawal burns eETH/weETH and mints a WithdrawRequestNFT; the queued ETH is then invisible to NAV, because the NFT is not ERC721Enumerable and exposes no owner→tokenIds view — a pure-view adapter cannot discover the positions. The owner feeds the request tokenId itself (submit(uint256), keyed on msg.sender); the adapter then values it entirely from live on-chain reads, re-verifying ownership on every read.

The cache stores only tokenIds, never amounts, so the feeder cannot fabricate value — at worst it omits an id (NAV under-counts), never over-counts. See Assisted balance adapters for the family trust model.


Positions returned

One Supplied leg per owned cached request, all sharing one static positionId:

Leg
PositionKind
isDebt
isLocked
Description

Per finalized request

Supplied

false

false

Native ETH from a finalized tokenId, withdrawable now

Per pending request

Supplied

false

true

Native ETH from a pending tokenId, still finalizing

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


Feed surface & assistant

The adapter exposes a small, permissionless, msg.sender-keyed surface (no roles, no account argument):

Function
Purpose

submit(uint256 tokenId)

Track tokenId for the caller (idempotent). Reverts CoordinateNotOwned if the caller doesn't own it, CoordinateCacheFull past MAX_TOKEN_IDS (256)

remove(uint256 tokenId)

Stop tracking tokenId for the caller. Reverts CoordinateNotCached if untracked, CoordinateStillLive if still owned (not yet claimed)

tokenIdsOf(address account)

View the candidate tokenIds cached for account (display/debug; not ownership-filtered)

clearAccount()

Drop all of the caller's cached ids (unconditional self-service GC)

_removable is "no longer owned by the caller": ether.fi's claimWithdraw burns the NFT, so a claimed id reads as unowned and becomes removable, while a still-owned (pending) id cannot be dropped (CoordinateStillLive). See the family write semantics.

The assistant (EtherFiWithdrawalQueueAssistant)

Use the assistant in place of ether.fi's LiquidityPool. Instead of requesting a withdrawal directly by calling the LiquidityPool's requestWithdraw (which would leave the resulting WithdrawRequestNFT invisible to NAV), you delegatecall the assistant: it forwards the same requestWithdraw and, in the same transaction, submits the returned requestId to the adapter. Claiming works the same way through claimWithdraw. It is a thin wrapper over ether.fi's own withdrawal channel — same action, plus the bookkeeping that makes the queued ETH visible to NAV.

Entrypoint
Does
Adapter call

requestWithdraw(uint256 amount)

Approves the LiquidityPool for the Safe's eETH and calls it with recipient = the Safe, minting the WithdrawRequestNFT to the Safe

submit(requestId)

claimWithdraw(uint256 tokenId)

Claims first (burns the NFT, ETH to the Safe), then untracks best-effort

remove(tokenId)

Both are onlyDelegateCall (a direct call reverts NotDelegateCall). Under delegatecall address(this) is the Safe, so ether.fi sees msg.sender = Safe (which its claim path hard-requires: ownerOf == msg.sender) and the adapter caches against that same Safe. The remove on claim is best-effort — it never blocks the claim: the assistant swallows the expected cache-state reverts (CoordinateNotCached for an id acquired outside the assistant or already cleared, CoordinateStillLive if somehow not yet claimed) and re-throws anything else.

Flow to request an ether.fi withdrawal: the Manager Safe drives the Portfolio Safe to delegatecall the assistant, which burns eETH via the LiquidityPool and submits the resulting requestId to the adapter.
Requesting a withdrawal. The Safe delegatecalls the assistant, which burns eETH via the LiquidityPool (minting the WithdrawRequestNFT) and submits the returned requestId to the adapter — all in one transaction.
Flow to claim an ether.fi withdrawal: the Safe delegatecalls the assistant, which claims (burning the NFT, ETH to the Safe) then best-effort removes the requestId from the adapter cache.
Claiming. The assistant claims first — burning the NFT and sending ETH to the Safe — then best-effort removes the requestId from the adapter's cache.

Balance calculation

Flowchart of the ether.fi Withdrawal Queue balance adapter: cached tokenIds are re-verified and valued into a PositionBalance entry.
ether.fi Withdrawal Queue adapter — how the underlying balances reported to the NAV Calculator are derived.

For each cached tokenId, the adapter re-verifies that account still owns the request and emits one leg valued at the ETH owed:

A request is claimable iff nft.isFinalized(tokenId); those use the protocol's getClaimableAmount and emit a leg with isLocked=false. Pending requests are valued live as min(amountOfEEth, amountForShare(shareOfEEth)) − feeGwei, converting the request's locked shares to ETH at the current rate, and emit a leg with isLocked=true. Each cached id is valued in isolation behind the base's per-coordinate self-staticcall (every external read try/catches to 0), so a stale, invalid, or reverting id contributes nothing and never drops the rest.


Identity

  • positionId: abi.encode(withdrawRequestNFT) (static; the ether.fi WithdrawRequestNFT address)

  • positionKind: Supplied

  • positionInstanceId: bytes32(tokenId) (the per-request WithdrawRequestNFT id; ephemeral — burned on claim)

  • Labels: the adapter implements positionLabels(positionId), returning ["Withdrawal Queue", "eETH"] (surfaced only on the verbose read path; full breadcrumb = ["ether.fi", "Withdrawal Queue", "eETH"]). The tokenId 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 owned 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

withdrawRequestNft_

ether.fi WithdrawRequestNFT — source of ownership + per-request data. Must be a contract

liquidityPool_

ether.fi LiquidityPoolamountForShare converts a request's shares to current ETH. Must be a contract

underlyingToken_

Payout asset — native ETH (ERC-7528 sentinel). Must be non-zero

navCalculator_

NAVCalculator address, used for asset-registry metadata. Must be a contract

There is no admin_ / assistant_ argument — the family is permissionless. The paired EtherFiWithdrawalQueueAssistant is deployed separately, pinned to this adapter (plus the LiquidityPool, WithdrawRequestNFT, and eETH token) at its own construction.


Registration

Registered as a plain adapter with addBalanceAdapters([adapter]) and removed with removeBalanceAdapters([adapter]). The coordinate cache is then kept current by the position owner's own transactions — typically via the assistant, delegatecalled inside the same requestWithdraw / claimWithdraw.

Last updated