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.
Type: Assisted adapter — permissionless,
msg.sender-keyed (AssistedCoordCache)protocolSubId:
keccak256("etherfi-withdrawal")Assistant:
EtherFiWithdrawalQueueAssistant.sol
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:
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):
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.
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.
WithdrawRequestNFT) and submits the returned requestId to the adapter — all in one transaction.removes the requestId from the adapter's cache.Balance calculation
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.fiWithdrawRequestNFTaddress)positionKind:
SuppliedpositionInstanceId:
bytes32(tokenId)(the per-requestWithdrawRequestNFTid; 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 inpositionInstanceId.
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
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.
Last updated