> 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/assisted-balance-adapters/etherfi-withdrawal-queue.md).

# ether.fi Withdrawal Queue

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.

* **Type:** [Assisted adapter](/funds/infrastructure/onchain-accounting/assisted-balance-adapters.md) — permissionless, `msg.sender`-keyed (`AssistedCoordCache`)
* **protocolSubId:** `keccak256("etherfi-withdrawal")`
* **Source:** [`EtherFiWithdrawalQueueAssistedBalanceAdapter.sol`](https://github.com/karpatkey/onchain-accounting/blob/main/src/balances/AssistedBalanceAdapters/EtherFiWithdrawalQueueAssistedBalanceAdapter.sol)
* **Assistant:** [`EtherFiWithdrawalQueueAssistant.sol`](https://github.com/karpatkey/onchain-accounting/blob/main/src/balances/AssistedBalanceAdapters/assistants/EtherFiWithdrawalQueueAssistant.sol)

{% hint style="info" %}
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](/funds/infrastructure/onchain-accounting/assisted-balance-adapters.md) for the family trust model.
{% endhint %}

***

## 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](/funds/infrastructure/onchain-accounting/assisted-balance-adapters.md#write-semantics-fail-loud).

### 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, `submit`s 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.

<figure><img src="/files/6Ymfdyh5N1YW4UjRVuNV" alt="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."><figcaption><p><strong>Requesting a withdrawal.</strong> The Safe delegatecalls the assistant, which burns eETH via the LiquidityPool (minting the <code>WithdrawRequestNFT</code>) and <code>submit</code>s the returned requestId to the adapter — all in one transaction.</p></figcaption></figure>

<figure><img src="/files/9B7rvQjnjShBgn7G3wnN" alt="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."><figcaption><p><strong>Claiming.</strong> The assistant claims first — burning the NFT and sending ETH to the Safe — then best-effort <code>remove</code>s the requestId from the adapter's cache.</p></figcaption></figure>

***

## Balance calculation

<figure><img src="/files/8JSX7ICPZMwwnYos0xC9" alt="Flowchart of the ether.fi Withdrawal Queue balance adapter: cached tokenIds are re-verified and valued into a PositionBalance entry."><figcaption><p>ether.fi Withdrawal Queue adapter — how the underlying balances reported to the NAV Calculator are derived.</p></figcaption></figure>

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

```
for each cached tokenId owned by account:
    if nft.isFinalized(tokenId):
        amount = nft.getClaimableAmount(tokenId); finalized = true
    else:
        amount = min(amountOfEEth, LiquidityPool.amountForShare(shareOfEEth)) - feeGwei; finalized = false   // current estimate
    emit leg { amount, isLocked = !finalized, positionInstanceId = bytes32(tokenId) }
```

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

```solidity
constructor(
    address withdrawRequestNft_,
    address liquidityPool_,
    address underlyingToken_,
    address navCalculator_
)
```

| Parameter             | Description                                                                                                |
| --------------------- | ---------------------------------------------------------------------------------------------------------- |
| `withdrawRequestNft_` | ether.fi `WithdrawRequestNFT` — source of ownership + per-request data. Must be a contract                 |
| `liquidityPool_`      | ether.fi `LiquidityPool` — `amountForShare` 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`.


---

# 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/assisted-balance-adapters/etherfi-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.
