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.
Type: Assisted adapter — permissionless,
msg.sender-keyed (AssistedCoordCache)protocolSubId:
keccak256("stakewise-v3-exit")Assistant:
StakeWiseV3ExitQueueAssistant.sol
This is not the StakeWise V3 page. That meta-adapter reports active vault staking positions (stakewise-v3-vaults); this one reports exit-queue positions (stakewise-v3-exit) for shares already on their way out. A fund using StakeWise needs both to see its full exposure.
Positions returned
Up to two legs per exit ticket that share that vault's positionId, split by claimability:
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:
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.
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.
receiver = the Safe and submits the resulting (vault, positionTicket, timestamp) tuple to the adapter — in one transaction.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
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:
SuppliedpositionInstanceId:
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"]ifreceiptSymbolis empty (surfaced only on the verbose read path; full breadcrumb =["StakeWise V3", "Exit Queue", "osETH"]). The receipt symbol comes from thereceiptSymbol_constructor arg, naming the receipt/LST token (osETH/osGNO), not the payout asset. The ticket id is not in the label — it rides inpositionInstanceId.
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
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