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
-
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 busyorleverage is enabledis usually too broad to reason about safe ordering.
-
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.
-
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.
-
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.
-
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
-
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 outas if the risky part of the operation is already over.
-
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 flowis not yet the same thing asthis edge should inherit the operation's live meaning right now.
- A reachable refund or hook is still weaker than
-
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?butwhy 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, andthis stage still deserves some nontrivial live authorityshould never collapse into one vaguewe are in cleanup nowassumption.
-
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.
- A refund path is not
-
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.
- If delivery must invoke
-
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 refundinverts the safe order.
2. Two-phase execution with a reduced post-delivery phase
- Split one operation into:
- privileged accounting phase;
- delivery / external interaction phase;
- 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 balancewithproceeds of my intended operationonce 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 privilegeorclear contextappears 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, orthis helper is activeafter 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, andwhat 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 accountingfromdeliver assetsfrompost-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, orthis 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, andthis stage may still keep powerful authorityare 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, andtrusted self-callare all weaker claims thanthe 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 closeexemplar:_transferOutETH()handed control to attacker code whileisLeverageEnabledstill exposed directVault.increasePosition()reachability. - f(x) Protocol is the strong
outer capability survived across attacker-recoverable external legsexemplar: 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 authorityexemplar: initiator-shapedswapDatastill executed fromRebalancerSpotwhile 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
- Start here when the sharper design question is
why was a powerful temporary capability still alive when value left the protocol through a refund, hook, swap, or other externally influenced delivery edge? - Pair with callback-frame-truth-security-object when the first job is defining the full in-flight frame that should have been committed.
- Pair with callback-frame authority reuse when the exploit classification is already clear and the design follow-up becomes
when exactly should that authority have already been extinguished? - Pair with receiver-hook-staged-delivery-and-single-accounting-ownership when the delivery edge is specifically a token or NFT receiver-hook boundary.
- Pivot to route-commitment-across-callback-frame-and-settlement-envelope when the sharper failure is not ordering alone but that a multi-step route or settlement envelope kept changing meaning across callback stages.
Internal linkages
- Exploit-side family: callback-frame-authority-reuse.
- Broader callback-design companion: callback-frame-truth-security-object.
- Receiver-hook delivery companion: receiver-hook-staged-delivery-and-single-accounting-ownership.
- Multi-step route companion: route-commitment-across-callback-frame-and-settlement-envelope.
- Best current refund-window exemplar: GMX V1.
- Best current nested-callback ordering exemplar: f(x) Protocol.
- Best current external-router ordering exemplar: Arcadia Finance.
- Best current receiver-hook cousin: Omni Protocol.
Control surface
-
The decisive control surface is the gap between
temporary privilege openedandtemporary privilege cleared, especially where that gap overlaps ETH refunds, token sends, safe transfers, external swaps, router callbacks, bridge deliveries, or other attacker-influenced execution edges. -
The recurring mistake is quietly promoting
a delivery edge is now reachableintothis edge is the canonical post-settlement surface for the frame, and then promoting that again intothis edge may still coexist with broad mutation or payout authority. -
Healthy systems narrow or extinguish authority before those edges fire, then settle only frame-local outputs afterward.
-
Sources:
- callback-frame-authority-reuse
- callback-frame-truth-security-object
- receiver-hook-staged-delivery-and-single-accounting-ownership
- gmx-v1-arbitrum-orderbook-refund-cross-contract-reentrancy-2025-07-09
- fx-protocol-double-flash-loan-stale-callback-context-victim-position-drain-near-miss-2025-06-17
- arcadia-finance-rebalancerspot-swapdata-router-context-spoof-cross-account-drain-2025-07-15
- omni-protocol-doodles-nftx-safe-transfer-double-reentrancy-2022-07-10
- https://blocksec.com/blog/gmx-incident-cross-contract-reentrancy-bypasses-a-four-year-old-guard
- https://www.chainsecurity.com/blog/f-x-protocol-circumventing-access-control-with-a-double-flash-loan-attack
- https://medium.com/immunefi/hack-analysis-omni-protocol-july-2022-2d35091a0109
-
Last reviewed: 2026-07-31 UTC