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

StakeWise V3 Exit Queue

An assisted adapter that reports the in-flight assets owed to an account in StakeWise V3's exit queue, fed on-chain by the owner Safe.

An assisted adapter that reports the in-flight assets owed to an account in StakeWise V3's exit queue. Calling enterExitQueue(shares, receiver) moves vault shares into the queue and returns a positionTicket; the queued value is then invisible to NAV — a position is identified only by that ticket (emitted in the ExitQueueEntered event) and there is no per-account enumeration. The owner feeds each (vault, positionTicket, timestamp) tuple itself (submit(address,uint256,uint256), keyed on msg.sender); the adapter then values it entirely from live on-chain reads.


Positions returned

Up to two legs per exit ticket that share that vault's positionId, split by claimability:

Leg
PositionKind
isDebt
isLocked
Description

Claimable

Supplied

false

false

Checkpointed (exited) staking asset past the claim delay, withdrawable now

Locked

Supplied

false

true

Still-queued shares (and exited-but-still-delayed assets) converted to staking asset

Both legs are denominated in the staking asset, native ETH on mainnet (carried in balanceAsset), with positionInstanceId = bytes32(positionTicket). A zero-amount leg is omitted; a ticket that values to 0/0 contributes no legs. The position is dropped entirely if that asset is not registered in NAVCalculator, if there are no live cached tickets, or if filtered out by assetFilter.


Feed surface & assistant

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

Function
Purpose

submit(address vault, uint256 positionTicket, uint256 timestamp)

Track one of the caller's own exit positions (idempotent). Reverts InvalidCoordinate if the vault is not StakeWise-registered, CoordinateNotOwned if the tuple resolves to no live owed amount for the caller, CoordinateCacheFull past MAX_POSITIONS (256)

remove(address vault, uint256 positionTicket, uint256 timestamp)

Stop tracking one of the caller's own exit positions (once claimed). Reverts CoordinateNotCached if untracked, CoordinateStillLive if still owed

positionsOf(address account)

View the candidate exit tuples cached for account (display/debug; not staleness-filtered)

clearAccount()

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

StakeWise exit positions have no ownerOf, so the on-chain ownership proxy is calculateExitedAssets(account, …) > 0: a tuple that isn't the caller's resolves to a zero owed amount and is rejected on submit (and dropped on read). _removable is "no longer owed" — a claimed/consumed ticket.

The assistant (StakeWiseV3ExitQueueAssistant)

Use the assistant in place of the StakeWise V3 vault. Instead of entering the exit queue directly by calling the vault's enterExitQueue (which would leave the resulting exit ticket invisible to NAV), you delegatecall the assistant: it forwards the same enterExitQueue and, in the same transaction, submits the returned (vault, positionTicket, timestamp) tuple to the adapter. Claiming works the same way through claimExitedAssets. It is a thin wrapper over StakeWise's own exit-queue channel — same action, plus the bookkeeping that makes the queued value visible to NAV.

Entrypoint
Does
Adapter call

enterExitQueue(vault, shares)

Enters the queue with receiver = the Safe, burning the Safe's shares and returning a ticket

submit(vault, positionTicket, block.timestamp)

claimExitedAssets(vault, positionTicket, timestamp, exitQueueIndex)

Claims exited assets to the Safe, then tries to remove the ticket

remove(vault, positionTicket, timestamp) — see below

Both are onlyDelegateCall. Under delegatecall address(this) is the Safe, so the vault sees msg.sender = Safe (which StakeWise's claim path requires — vault shares are non-transferable and the receiver must be the claimer) and the adapter caches against that same Safe. On claim the ticket is deregistered best-effort — removal never blocks the claim itself: a partial claim leaves the position still owed (adapter reverts CoordinateStillLive → keep it tracked), and an untracked ticket reverts CoordinateNotCached (nothing to untrack); the assistant swallows both and re-throws anything else. So the ticket is effectively dropped only once fully consumed.

Flow to enter the StakeWise exit queue: the Manager Safe drives the Portfolio Safe to delegatecall the assistant, which enters the vault's exit queue (receiver = Safe) and submits the (vault, ticket, timestamp) tuple to the adapter.
Entering the exit queue. The Safe delegatecalls the assistant, which enters the vault's exit queue with receiver = the Safe and submits the resulting (vault, positionTicket, timestamp) tuple to the adapter — in one transaction.
Flow to claim from the StakeWise exit queue: the assistant claims exited assets to the Safe then best-effort removes the ticket — kept if still owed (CoordinateStillLive), dropped once fully consumed.
Claiming. The assistant claims exited assets to the Safe, then best-effort removes the ticket: a partial claim keeps it tracked (CoordinateStillLive); it is dropped only once fully consumed. A re-queued remainder lands under a new ticket the owner must submit separately.

Re-queued remainder lands under a new ticket. When StakeWise re-queues a not-yet-checkpointed remainder, it does so under a new positionTicket — a distinct coordinate. The assistant only manages the ticket it was given; the caller must submit the new ticket separately (from the vault's re-queue event).


Balance calculation

Flowchart of the StakeWise V3 Exit Queue balance adapter: cached exit tuples are valued via calculateExitedAssets into a PositionBalance entry.
StakeWise V3 Exit Queue adapter — how the underlying balances reported to the NAV Calculator are derived.

For each cached tuple the adapter values the position by calling the vault with receiver = account, splitting it into a claimable and a locked portion and emitting up to two legs per ticket:

When getExitQueueIndex(ticket) < 0 the position is not yet checkpointed, so it is fully pending and contributes only to the locked leg. Otherwise calculateExitedAssets splits it: the already-exited assets and the still-queued leftShares (converted at the current rate via convertToAssets). The exited portion is reported claimable (isLocked=false) only once StakeWise's post-checkpoint claim delay has elapsed (block.timestamp >= timestamp + EXIT_CLAIM_DELAY); before that it stays locked. The still-queued remainder is always locked (isLocked=true). Both legs carry positionInstanceId = bytes32(positionTicket). Because the call passes receiver = account, a tuple that does not belong to account resolves to a zero exit request and is dropped — this is the on-chain ownership check. Each cached tuple is valued in isolation behind the base's per-coordinate self-staticcall (try/catch to 0), so one bad tuple never drops the rest.


Identity

  • positionId: abi.encode(vault) (static, per vault; the StakeWise V3 vault address)

  • positionKind: Supplied

  • positionInstanceId: bytes32(positionTicket) (the per-ticket exit coordinate; ephemeral — both legs of a ticket carry the same instance id)

  • Labels: the adapter implements positionLabels(positionId), returning ["Exit Queue", "<receiptSymbol>"] — e.g. ["Exit Queue", "osETH"] on mainnet — falling back to the bare ["Exit Queue"] if receiptSymbol is empty (surfaced only on the verbose read path; full breadcrumb = ["StakeWise V3", "Exit Queue", "osETH"]). The receipt symbol comes from the receiptSymbol_ constructor arg, naming the receipt/LST token (osETH/osGNO), not the payout asset. The ticket id is not in the label — it rides in positionInstanceId.

A vault's tickets share that vault's positionId; a ticket's claimable and locked legs are distinguished for display by the isLocked field (false = exited and past the claim delay/withdrawable now, true = still queued or exited-but-delayed). The owed staking asset (native ETH on mainnet) rides in balanceAsset. The reconciliation key is keccak256(abi.encode(chainId, protocolSubId, positionId, balanceAsset.asset, positionKind)) — it excludes both isLocked and positionInstanceId, so per vault all of an account's tickets (claimable and locked alike) reconcile into one coordinate and sum; read isLocked per leg for the claimable-vs-locked split.


Constructor

Parameter
Description

vaultsRegistry_

StakeWise VaultsRegistry — gates which vaults the cache accepts (write-time hygiene). Must be a contract

underlyingToken_

Staking asset the queue pays out — native ETH (sentinel) on mainnet. All accepted vaults must settle in this asset. Must be non-zero

receiptSymbol_

Display symbol of StakeWise's receipt/LST token for this chain ("osETH" mainnet, "osGNO" Gnosis); feeds the label token. May be empty — label then falls back to the bare ["Exit Queue"]

navCalculator_

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

exitClaimDelay_

StakeWise's post-checkpoint claim delay in seconds (24h = 86400 on V3 mainnet). Gates when an exited portion becomes claimable. Must be non-zero

There is no admin_ / assistant_ argument — the family is permissionless. The paired StakeWiseV3ExitQueueAssistant is deployed separately, pinned to this adapter.

Mainnet only. Every StakeWise V3 vault on mainnet settles in ETH; do not deploy against a non-ETH underlyingToken on a chain with a different staking asset.


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 enterExitQueue / claimExitedAssets.

Last updated