Temporary Capability Closure Before External Delivery

  • Name: temporary capability closure before external delivery
  • Category: callback-systems reference / privilege-window ordering discipline / refund-and-hook safety note / positive-design companion to callback-frame exploits
  • Summary: Many modern callback losses do not start with an obviously fake caller. They start because a protocol opens a temporary capability for one bounded internal purpose — isLeverageEnabled, inFlashLoan, this helper may self-call, this account may still be treated as mid-close, this vault may finish one staged withdrawal — and then hands control to an externally influenced delivery surface before that capability is consumed. ETH refunds, token sends, safe-transfer receiver hooks, swap callbacks, router dispatch, and other user-reachable deliveries are not cleanup steps outside the threat model; they are new control-transfer edges. Healthy designs extinguish the privileged window before those edges fire, or reopen a narrower post-delivery capability whose scope is too small to recover the earlier authority.
  • Why this note exists:
    • Callback-frame notes explain what exact frame should have been authenticated, but a second recurring design question is when the privileged frame should already have been dead.
    • Teams often reason correctly about the intended actor or provider, then still lose because refunds, hooks, or other delivery paths happen while the powerful capability remains open.
    • GMX V1, f(x), and Arcadia preserve three crisp variants of the same lesson: a user-directed ETH refund inside an open keeper-execution privilege, a nested flash-loan / swap leg that could still consume a stale callback capability because the outer frame had not been closed yet, and a router / adapter leg that inherited asset-manager authority because the rebalance frame stayed too broad while external execution was still live.

The five joints a healthy temporary-capability design should keep fused

  1. Explicit capability object

    • The protocol should know what narrow privilege is live right now: one keeper-managed order execution, one flash-loan repayment stage, one position-close continuation, one staged delivery finalization step.
    • router is busy or leverage is enabled is usually too broad to reason about safe ordering.
  2. Capability open only after preconditions are committed

    • The protocol should commit the actor, target, stage, payout bucket, and other critical frame fields before the privilege becomes spendable.
    • Otherwise the later delivery edge is already inheriting an underspecified authority.
  3. Capability closes before externally influenced delivery

    • ETH refunds, token sends, safe transfers, external swaps, router callbacks, and helper dispatch are all externally influenced control-transfer surfaces.
    • If the privileged window stays open across one of those surfaces, the protocol has created a second chance to spend the same authority under new attacker-shaped conditions.
  4. Post-delivery work uses a narrower new capability, not the old one

    • If the flow really must continue after a refund, hook, or external swap, the resumed step should use a freshly committed post-delivery frame whose meaning is strictly smaller than the earlier privilege.
    • Healthy systems do not let the resume step inherit all of the earlier authority just because the overall operation is not finished yet.
  5. Settlement only spends frame-local outputs

    • Final payout should spend the delta produced by the intended frame, not whatever balances happen to remain after nested delivery or callback execution.
    • Otherwise even a brief privilege-window leak turns into a direct theft rail.

Three delivery-edge promotions reviewers should not collapse

  1. Did a delivery-capable edge become reachable at all?

    • ETH refunds, token sends, NFT safe transfers, swap callbacks, bridge deliveries, and router / adapter legs are the weak first fact: the operation can now hand control or value to something outside the protocol’s narrow local accounting frame.
    • Teams often stop here and quietly treat now we are just paying out as if the risky part of the operation is already over.
  2. Did that edge become the canonical delivery stage for this exact frame?

    • A reachable refund or hook is still weaker than this exact recipient / adapter / route leg is now the one legitimate post-settlement delivery surface for this exact actor, target, and payout object.
    • GMX, Arcadia, and Omni are useful reminders that a delivery edge is part of the flow is not yet the same thing as this edge should inherit the operation's live meaning right now.
  3. Did that admitted delivery stage still deserve any broad mutation or payout authority?

    • Even once the right delivery edge is identified, the protocol can still over-authorize it by leaving leverage, flash-loan, router, manager, or settlement-bucket authority alive while the edge runs.
    • The sharp review question is not only was this the right place to send value? but why did this edge still coexist with authority strong enough to mutate more than the narrow delivery delta?

What sound design looks like

  • Healthy systems keep delivery reachability, canonical delivery meaning, and residual authority as three separate questions

    • A delivery edge exists, this is the exact post-settlement delivery stage for this frame, and this stage still deserves some nontrivial live authority should never collapse into one vague we are in cleanup now assumption.
  • Privilege first narrows, then value leaves

    • Before ETH, tokens, NFTs, or other externally receivable value leaves the protocol, the broad internal capability that made the operation possible is already consumed or reduced to a harmless residual step.
  • Refunds are treated as callback edges

    • A refund path is not after the real work; it is another place where attacker-controlled code may run.
    • If the protocol would not tolerate the same authority during a flash-loan callback, it should not tolerate it during an ETH refund either.
  • Safe transfer and receiver hooks are staged explicitly

    • If delivery must invoke onERC721Received, onERC1155Received, token hooks, or similar logic, accounting and privilege windows are already aligned before the hook runs.
    • receiver-hook-staged-delivery-and-single-accounting-ownership is the narrower companion when the real question is delivery ownership and staged hook settlement.
  • External swap / bridge / adapter legs do not inherit upstream authority by default

    • If an operation must leave the protocol for a swap or adapter step and then resume, the resumed stage should prove a fresh post-external boundary rather than relying on the original pre-external permission still being live.
  • Cleanup uses a weaker authority than the main action

    • Post-close payout, residual refund, and bookkeeping cleanup should be the least privileged part of the flow, not the part that still carries full access to core mutation entrypoints.

Useful positive design shapes

1. Close-before-transfer ordering

  • Consume or clear the broad capability before any ETH refund, token send, NFT transfer, or other user-directed delivery happens.
  • Then perform delivery as an unprivileged or narrowly privileged action.
  • GMX V1 is the clean reminder that close capability after refund inverts the safe order.

2. Two-phase execution with a reduced post-delivery phase

  • Split one operation into:
    1. privileged accounting phase;
    2. delivery / external interaction phase;
    3. optional post-delivery cleanup with a new, weaker capability.
  • The second and third phases should be unable to reach the original mutation surfaces.

3. Escrowed outputs plus explicit claim step

  • Instead of sending assets during the privileged frame, record owed outputs in escrow and let the user claim them through a separate low-privilege path after the core state transition is complete.
  • This often removes the incentive to keep powerful authority live across delivery.

4. Selector-minimized resume surfaces

  • If a callback or self-call must resume execution after an external leg, constrain it to one stage-specific selector set or one payload hash committed before the external handoff.
  • address(this).call(userData) while a powerful capability remains open is almost always overbroad.

5. Frame-local payout accounting

  • Track exactly what value this frame created or released, and settle only that amount.
  • Never equate all assets currently in router / vault balance with proceeds of my intended operation once nested execution is possible.

6. Capability stratification by stage

  • Replace one long-lived boolean with several single-use stages: prepare, settle, deliver, claim, each with narrower permissions.
  • That way a delivery-stage callback cannot accidentally inherit prepare- or settle-stage authority.

Design questions worth asking first

  • What exact capability is live when the protocol sends this ETH refund, token transfer, NFT safe transfer, or external swap call?
  • Why does that capability still need to exist at the moment value leaves the protocol?
  • If the flow must continue after delivery, what new and narrower capability proves the resumed step rather than reusing the old one?
  • Could the recipient, swap counterparty, adapter, or hook implementation run code before the privilege closes?
  • When execution resumes, does payout use frame-local deltas or ambient balances / mixed module state?
  • Could a second provider, helper, or nested call consume the same still-live capability before outer cleanup returns?

Red flags that usually mean ordering is doing the real damage

  • disable privilege or clear context appears only after an ETH send, token send, or callback-prone external call
  • comments or docs describe refunds as harmless cleanup even though they are externally controlled transfers
  • one broad capability window spans both core mutation and user-facing payout
  • the protocol relies on onlySelf, trusted callback sender, or this helper is active after an external leg that can still shape calldata or nested provider choice
  • final payout is balanceOf(this) or another ambient bucket rather than a frame-local amount
  • nested adapters, swap routers, or flash-loan providers can run while an earlier stage’s authority is still live

Failure-prevention checklist

  • Enumerate every external-delivery edge in privileged flows: ETH refunds, token sends, safe NFT transfers, receiver hooks, external swaps, bridge deliveries, router callbacks, and helper dispatch.
  • For each edge, answer three separate questions instead of one: did a delivery edge become reachable, did this exact edge become canonical for this frame, and what residual authority, if any, still survives while it runs.
  • For each edge, prove the broadest capability has already been consumed, cleared, or reduced before control can reach attacker-influenced code.
  • Replace module-wide inFlashLoan, isLeverageEnabled, currentlyExecuting, or similar booleans with stage-specific single-use capabilities whose lifetime ends before user-directed delivery.
  • Separate settle accounting from deliver assets from post-delivery cleanup; if one phase needs to resume after another, commit a new weaker frame rather than keeping the earlier privilege alive.
  • Route payouts through frame-local accounting, escrow claims, or explicitly tracked deltas instead of sweeping ambient balances after nested execution.
  • Treat refunds and residual sends as security-critical paths in code review, testing, and monitoring, not as low-risk UX conveniences.
  • Constrain any post-external resume path to a tight selector set or pre-committed payload hash; do not allow externally influenced callbacks to choose fresh privileged meaning.
  • Fuzz and adversarially simulate malicious recipients, safe-transfer hooks, nested flash loans, nested swap callbacks, and mixed-provider delivery while checking that no higher-stage capability survives into later stages.
  • Add telemetry for privileged flows that make external calls before clearing context, and alert on nested callback / refund / hook re-entry while temporary authority is still active.
  • Prefer pull-based claims or delayed withdrawal queues when safe ordering would otherwise require leaving too much authority live across delivery.

Audit prompts

  • Which exact temporary authority is still live at each external-delivery edge in this flow?
  • Which of the three delivery-edge questions does each control actually answer: a delivery edge exists, this exact edge is canonical for this frame, or this edge may still carry some residual authority?
  • Does any ETH refund, token transfer, safe transfer, or external adapter call happen before that authority is cleared?
  • If the protocol intentionally resumes after an external leg, what locally committed object proves the resumed stage is narrower than the original one?
  • Can a malicious recipient or callback counterparty use the still-live capability to reach a mutation path that was only meant for the earlier stage?
  • Are refunds, residual sends, and payout helpers reviewed under the same threat model as flash-loan callbacks and token hooks?
  • Does final settlement spend frame-local deltas, or could nested execution pollute the bucket that later gets paid out?
  • Can two providers or two helper paths overlap such that delivery during one stage consumes authority opened for another?
  • If this capability were cleared before the delivery edge, what legitimate behavior would break — and is that behavior really worth the expanded attack surface?
  • Which tests prove that a malicious receiver cannot turn cleanup or payout into a privileged resume surface?
  • What operational telemetry would show a live exploit early: refunds occurring inside active leverage windows, nested flash-loan callbacks before context clear, or external delivery while a privileged stage bit remains set?

Lessons that should already be standard

  • External delivery is part of privileged control flow, not the part that happens after security stops mattering.
  • A delivery edge exists, this is the intended delivery stage, and this stage may still keep powerful authority are different claims. Healthy systems prove each one separately.
  • A refund is just a callback edge with nicer product language.
  • Long-lived temporary authority is usually a design smell. The longer a capability survives, the more unrelated control-transfer edges it will accidentally cover.
  • If a flow must resume after an external step, that resumed step deserves its own smaller capability, not a free ride on the old one.
  • Payout accounting and privilege ordering are the same problem once nested execution can drag third-party value into the settlement bucket.
  • Trusted recipient, trusted provider, and trusted self-call are all weaker claims than the earlier capability was already dead before this delivery happened.
  • Safer UX often comes from weaker privilege: escrowed claims and explicit later withdrawals may be less elegant than inline refunds, but they dramatically shrink callback surface.
  • The clean target is simple: powerful capability windows should expire before value leaves the protocol through attacker-influenced edges.

Negative exemplar cluster: what bad ordering looks like

  • GMX V1 is the clean ETH refund before privilege close exemplar: _transferOutETH() handed control to attacker code while isLeverageEnabled still exposed direct Vault.increasePosition() reachability.
  • f(x) Protocol is the strong outer capability survived across attacker-recoverable external legs exemplar: the router kept a broad flash-loan context live across nested provider and swap-driven control transfer, so a later genuine callback inherited earlier authority.
  • Arcadia Finance is the strong external router leg inherited live asset-manager authority exemplar: initiator-shaped swapData still executed from RebalancerSpot while the rebalance frame was live, so router execution inherited cross-account power that should already have been extinguished or reduced before the external leg.
  • Omni Protocol is the receiver-hook cousin: NFT delivery ran before collateral-use state had been coherently retired, so the delivery edge inherited meaning it should not have had.

How to use this note with the exploit corpus

Internal linkages

Control surface