Callback-Frame Authority Reuse / Stale In-Flight Execution Context
- Name: callback-frame authority reuse / stale in-flight execution context
- Category: exploit pattern / callback-authenticity failure / temporary-capability leakage / execution-context reuse
- Summary: These exploits happen when a protocol collapses three different questions into one vague
callback is activestory. First,some callback-capable lane is live right now; second,that lane is the exact provider / actor / target / stage frame this resumed path is supposed to inherit; and third,that exact frame still deserves authority over this payout bucket, ledger, or settlement side effect. A boolean likeinFlashLoan,isLeverageEnabled,helper is executing, oronlySelfafter a trusted callback path is not enough once routers, keeper flows, refund paths, or nested providers can re-enter before the frame closes. A real callback, refund, or nested control-transfer then inherits stale authority from a different provider, actor, target, stage, or payout bucket and uses that authority to run a privileged path the original flow never meant to expose. - What it is:
- The protocol opens a temporary execution frame with some privileged meaning: repay one flash loan, execute one keeper-managed order, resume one user close flow, or finish one bounded helper action.
- Instead of committing the full frame, the code authenticates only a coarse ambient condition such as
some flash loan exists,leverage is enabled, orthis router may self-call right now. - Control leaves the protocol through a callback, refund, external swap, or other user-reachable hook before that temporary authority is fully consumed.
- A nested path, second provider, or attacker-shaped self-call reuses the still-live authority to act on a different actor, target object, internal selector, or accounting bucket than the original frame intended.
- The source callback can be genuine; the bug is that the protocol authenticated the wrong object.
- Typical preconditions:
- A router, helper, keeper path, or refund flow uses a temporary privilege window rather than a fully bound per-operation frame.
- The protocol supports more than one callback-capable provider, more than one internal resume path, or more than one target object that the same ambient flag can cover.
- Some external control-transfer happens before the privilege window closes.
- Callback dispatch is broader than the real intended stage, often through raw
address(this).call(...), module-wide access gates, or direct entrypoints left reachable while the frame is live. - Final payout or accounting logic trusts ambient balances or cross-module state as if they still belonged exclusively to the original frame.
- Common vulnerable surfaces:
- flash-loan routers that use one shared
inFlashLoanbit across multiple lenders or callback facets - keeper-managed order-execution paths that refund ETH or tokens before closing a temporary execution privilege
- delegated helper contracts that let callbacks or self-calls pick the target account, position, selector, or settlement bucket after the frame starts
- modular systems where one contract opens the capability window and another contract spends it without proving the same frame fields
- payout paths that sweep
balanceOf(this)or trust mixed cross-module state at the end of a callback-heavy flow
- flash-loan routers that use one shared
- Exploit sequence:
- The attacker identifies a temporary callback or execution window guarded only by a generic active-state check.
- They find a way to regain control before that window closes: a refund, flash-loan callback, external swap leg, token transfer hook, or router-managed self-call.
- While the original frame is still live, they trigger a second path that also satisfies the coarse guard.
- That second path reuses stale authority from the original frame but supplies a different provider, actor, target object, selector, or payout meaning.
- The protocol executes a privileged state transition that was supposed to be reachable only inside the original frame.
- The attacker monetizes the result through stolen collateral, corrupted accounting, or a payout bucket that now contains assets or marks belonging to the wrong execution path.
- Variants:
- Cross-provider callback reuse — one lender’s callback flag authorizes another lender’s later callback. f(x) Protocol is the crisp preserved case.
- Refund-inside-open-capability leakage — a user-directed refund or transfer fires while a privileged order-execution window is still open. GMX V1 is the clean exemplar.
- Broad self-call resume surface — a trusted callback may self-call the router, but the callback payload still chooses a fresh privileged selector or target object.
- Payout-bucket contamination — the protocol ends the flow by sweeping whatever balance or accounting state is present after nested execution, so even a small frame-authenticity mistake becomes a direct theft path.
Three callback-frame questions healthy systems should not collapse
-
Did a real callback-capable lane become live at all?
- This is the weak first question ambient booleans usually answer: a flash loan started, a keeper flow opened, a helper is mid-execution, or a router self-call is temporarily allowed.
- Many systems stop here and quietly treat
some callback context existsas if the hard work were done.
-
Did that live lane authenticate this exact provider / actor / target / stage frame?
- A genuine callback or live helper window is still weaker than
this exact lender may resume for this exact user, this exact position, this exact pair, at this exact stage. - f(x), Exactly, Predy, and Arcadia all preserve different ways this second promotion failed.
- A genuine callback or live helper window is still weaker than
-
Did that exact frame still deserve authority over this payout bucket, ledger meaning, or settlement side effect?
- Even once the right frame is identified, the protocol can still over-authorize it by letting the resumed path spend ambient balances, mixed pair inventory, or a broader manager identity than this stage should retain.
- GMX and Predy are the sharp reminders that
callback was realand evenframe was plausibly realstill do not prove the resulting payout or ownership scope stayed local to the intended frame.
- Detection heuristics:
- Search for booleans or broad gates like
inFlashLoan,isLeverageEnabled,currentlyExecuting,onlySelf, ortrusted callback senderand ask what exact actor, target, stage, and payout bucket they do not prove. - Trace every external call, refund, token send, swap hook, or self-call dispatch that occurs before those flags are cleared.
- If multiple callback providers or multiple internal selectors share one ambient context variable, assume the design is guilty until proven otherwise.
- Review payout logic at the end of callback-heavy flows: if it spends ambient balances or mixed module state instead of frame-local deltas, the callback-authenticity bug may already be monetizable.
- Ask whether the callback can redefine the acting account, target position, or resumed selector through calldata that was never pre-committed locally.
- Search for booleans or broad gates like
- Quick classifier against nearby families:
- Stay here when the callback source may be real but the decisive failure is that one still-live execution frame leaked provider, actor, target, stage, or payout authority into another path.
- Use reentrancy when the sharpest preserved lesson is still the classic one: control returned before balances, debt, or permissions were finalized, even if no narrower frame-authenticity story is needed.
- Use callback-frame-truth-security-object when the main task is positive design: what exact callback frame should have been committed and consumed once.
- Use route-commitment-across-callback-frame-and-settlement-envelope when the sharper design task is end-to-end: how that committed callback frame should have stayed pinned to one later settlement envelope, payer source, or route-local payout bucket instead of drifting into ambient route state.
- Use account-confusion-root-of-trust-substitution when a counterfeit market, bank, jar, or helper had already become the trusted root object before the callback-frame leakage mattered.
- Use trusted-execution-envelope-corruption when the real bug is not stale in-flight authority but corrupted protocol-authored settlement bytes consumed downstream.
- Use balance-delta provenance failure when the callback frame was genuine enough but the sharper preserved loss is that aggregate conservation hid which pair, tranche, or position still owned the assets afterward. Predy is the clean seam case: callback-frame leakage opened the door, then pair-ledger provenance loss turned
same pool balances returnedinto false proof of honest settlement.
What sound design looks like
-
Healthy systems keep three facts distinct on purpose:
a callback lane is live,this exact provider / actor / target / stage frame is the one that lane authenticates, andthis exact frame still deserves only this narrow payout or settlement authority. -
The protocol commits one explicit callback frame before control leaves: exact provider, acting account, target object, stage, allowed resume selector or payload hash, and the payout bucket the resumed path may touch.
-
That frame is single-use and stage-bound. If the flow must cross several callback edges, each stage gets its own committed object rather than sharing one ambient
inFlashLoan,onlySelf, orisLeverageEnabledpermission bit. -
Refunds, token sends, and user-directed transfers happen only after the privileged frame is already consumed, or they run inside a narrower frame that cannot reopen earlier authority. temporary-capability-closure-before-external-delivery is the sharper positive-design companion when the main question becomes
why was any powerful capability still alive once value started leaving through an externally influenced delivery edge? -
When flexible external routing is unavoidable, execute it through an explicitly unprivileged trampoline or sandbox that only holds the swap inventory for that leg. The router should see a narrow helper context, not the full asset-manager / keeper / rebalancer identity that opened the frame. privilege-stripped-external-execution-and-inventory-scoped-trampolines is the dedicated healthy-side note for that architectural move.
-
Internal self-calls are treated as externally dangerous unless the exact resume selector and arguments were pre-committed.
address(this)is not a substitute for frame authentication. -
Final settlement uses frame-local deltas and ownership scopes instead of sweeping whatever ambient balances happen to remain after nested execution.
-
When several providers, helpers, or modules can open similar windows, storage layout itself should prove that those windows cannot authenticate one another by accident.
-
The clean target is
one callback source, one actor, one target, one stage, one payout meaning, consumed exactly once. -
Defenses:
- Commit one explicit callback frame before external control leaves: provider, actor, target object, stage, allowed selector or payload hash, and payout bucket.
- Consume that frame once and clear it before issuing refunds, user-directed transfers, or nested provider interactions.
- Use provider-specific or stage-specific callback slots instead of one shared ambient boolean.
- Do not let trusted callbacks choose fresh privileged internal selectors or target objects from attacker-shaped calldata unless those values were pre-committed.
- If arbitrary router flexibility is a product requirement, route that leg through an unprivileged trampoline that cannot satisfy account-manager or callback-sensitive authorization checks elsewhere in the system.
- Close capability windows before any externally controlled transfer whenever the flow allows it.
- Settle from frame-local deltas rather than
whatever balance is leftafter nested execution.
Failure-prevention checklist
- Review privileged callback flows in three passes instead of one:
what callback-capable lane is live,what exact frame that lane authenticates, andwhat exact payout / ledger authority that frame still deserves. - Write down the full callback frame the protocol intends to honor: provider, acting account, target object, stage, allowed resume selector or payload hash, and the exact payout bucket that may be touched before control returns.
- Commit that frame in trusted local state before the first external handoff, and treat every later callback, refund, self-call, or nested provider interaction as an attempt to consume that exact object rather than as proof that
some callback is active. - Use provider-specific or stage-specific callback slots instead of one module-wide boolean. If multiple lenders, keepers, helpers, or routers can be live in the same module, require the storage shape to prove which one owns the privilege window.
- Close temporary capability windows before user-directed refunds, ETH sends, token transfers, or other externally controlled interactions whenever the flow allows it. A refund inside a live frame is another callback edge, not a harmless cleanup step.
- Bind actor and target identity before the callback and never let callback calldata redefine them. A genuine lender or router callback should not get to choose a new victim account, position id, selector, or market after the frame has opened.
- Route internal self-calls through tight selector allowlists or pre-committed payload hashes rather than generic
address(this).call(userData)dispatch while privileged state is live. - Keep payout accounting frame-local. If settlement or withdrawal logic still sweeps
balanceOf(this)or trusts a mixed cross-module mark, the frame-authenticity bug is already one nested call away from theft. - Adversarially test cross-provider nesting, refund-inside-frame paths, helper self-calls, target rebinding, and payout-bucket contamination rather than only same-function recursion or the happy-path callback.
- Review kill switches and rollout controls on callback-heavy helpers and routers. Once stale-frame reuse is public, the exploit race usually runs at user-approval or protocol-solvency speed, not at leisurely patch speed.
Audit prompts
- Which of the three questions does this control actually answer:
some callback lane is live,this exact frame is live, orthis exact frame may still touch this payout / settlement bucket? - What exact fields does the ambient guard prove, and what critical frame fields does it leave unconstrained: provider, actor, target, stage, selector, or payout bucket?
- Can a genuine callback from provider A consume authority that was opened for provider B, or can one stage reuse authority that was meant for another stage in the same module?
- Does any refund, token send, ETH transfer, swap hop, bridge callback, or self-call happen before the frame is cleared?
- Can callback calldata redefine the acting account, position, order, market, selector, or settlement bucket after the protocol has already decided that a trusted frame is live?
- Are provider-specific and stage-specific frame slots actually distinct in storage, or does one boolean / enum / helper-wide gate silently cover several incompatible meanings?
- If the callback is genuine, what still prevents it from reaching the wrong privileged path? Is there a selector allowlist, payload hash, nonce-like frame id, or only
onlySelf/trusted sender? - When the flow ends, does payout come from frame-local deltas or from ambient balances / cross-module accounting that another nested path could have polluted?
- Could one frame be consumed more than once, partially consumed and then resumed, or left live across nested provider interactions because cleanup only happens at outer-function return?
- Which adversarial tests prove that a legitimate callback cannot migrate authority across accounts, positions, markets, or payout buckets inside one transaction?
- If this callback helper were under active exploitation today, what concrete mechanism stops further drains quickly: onchain pause, route disablement, approval revocation campaign, or nothing until users notice?
Lessons that should already be standard
-
Some callback context existsis the beginning of review, not the end. Healthy systems keeplane is live,exact frame is live, andframe still deserves this authorityas separate proofs. -
A real callback is not enough. The question is never only
did a trusted provider call back?butdid this exact provider call back for this exact actor, target, stage, and payout frame? -
inFlashLoan,isLeverageEnabled, and similar booleans are liveness hints, not capability proofs. Modern router and helper exploits keep showing that one ambient bit is weaker than the operation it is supposed to authenticate. -
Refunds and cleanup transfers belong inside the threat model. If a privileged window stays open across an ETH send or token transfer, the protocol has created another resume surface for stale authority.
-
onlySelfis not a trust boundary when attacker-shaped calldata still chooses what self-call to make. A router calling itself is safe only if the allowed resume path was already committed. -
Final payout is part of callback authentication. If the protocol spends whatever assets happen to be lying around after nested execution, it is authenticating the wrong balance bucket even if the callback source itself was genuine.
-
Mixed-provider and mixed-helper systems need single-use frame objects, not broad module state. One honest callback plus one stale context bit is often enough to turn a bounded helper into a cross-account drain primitive.
-
Attackers do not need to forge the lender, keeper, or router when the protocol already widened that actor’s authority beyond one in-flight operation.
-
Router allowlistis not the whole answer when the deeper bug is privilege inheritance. Sometimes the safer move is architectural: make the router call from a helper that cannot possibly be mistaken for the privileged frame owner elsewhere. -
The clean design goal is stronger than
trusted callback plus nonReentrant: one callback source, one actor, one target, one stage, one allowed resume path, and one payout meaning, consumed exactly once. -
High-signal incidents:
- Predy Finance PredyPool callback-locker cross-pair liquidity migration drain (2024-05-14) — the attacker opened a legitimate trade callback on a self-created pair, inherited pool-global
lockerauthority during that frame, migrated liquidity from an older pair into the new pair, and later withdrew it under ordinary supply accounting. - f(x) Protocol double-flash-loan stale-callback-context victim-position drain near miss (2025-06-17) — one shared
flashLoanContextbit let a genuine Balancer callback inherit Morpho-framed authority and self-call the privileged close hook against a victim-approved position. - [[../../../incidents/callbacks-reentrancy/arcadia-finance-rebalancerspot-swapdata-router-context-spoof-cross-account-drain-2025-07-15|Arcadia Finance RebalancerSpot
swapDatarouter-context spoof / cross-account drain (2025-07-15)]] — initiator-shaped router calldata executed inside a liveflashAction()rebalance frame, so attacker-controlled router code inherited RebalancerSpot authority against other Arcadia Accounts that already trusted that rebalancer as asset manager. - GLP price manipulation (2025-07-09) — an ETH refund callback fired while
isLeverageEnabledwas still open, so the attacker directly reachedVault.increasePosition()outside the intended keeper path and corrupted split short-accounting state. - Exactly Protocol DebtManager malicious-market permit account-rebinding reentrancy drain (2023-08-18) — a mixed cousin where fake-market trust substitution and reentrancy rebound the helper’s acting-account frame before the privileged flow resumed against real markets.
- Predy Finance PredyPool callback-locker cross-pair liquidity migration drain (2024-05-14) — the attacker opened a legitimate trade callback on a self-created pair, inherited pool-global
-
Why this pattern matters:
- Modern router and helper exploits often survive naive review because every local check looks plausible: the lender was real, the keeper path was real, the callback sender was real, the self-call came from
address(this). The system still loses because those facts did not prove the same in-flight operation. - The family is a useful bridge between classic reentrancy and the more prescriptive callback-frame design notes: it preserves the exploit-side mechanic without collapsing back into
just add nonReentrant. - It also keeps payout semantics in scope. Many of these incidents become expensive not when the stale frame first opens, but when the protocol later treats mixed balances or cross-module marks as if they still belonged to one coherent execution path.
- Modern router and helper exploits often survive naive review because every local check looks plausible: the lender was real, the keeper path was real, the callback sender was real, the self-call came from
Internal linkages
- Parent family: reentrancy.
- Positive-design companion: callback-frame-truth-security-object.
- Ordering-and-delivery companion: temporary-capability-closure-before-external-delivery.
- Architecture companion for flexible routers / plugins: privilege-stripped-external-execution-and-inventory-scoped-trampolines.
- End-to-end routing companion: route-commitment-across-callback-frame-and-settlement-envelope — use this when the callback-frame question widens into
how should one route keep callback truth and later settlement truth fused across the whole execution lifecycle? - Adjacent pattern: account-confusion-root-of-trust-substitution — counterfeit root objects often create the stale callback frame, but this note starts once the wrong or overly broad in-flight authority is what actually gets reused.
- Adjacent pattern: trusted-execution-envelope-corruption — both families authenticate the wrong execution object, but that note is about corrupted protocol-authored bytes rather than stale callback authority.
- Adjacent pattern: balance-delta provenance failure — use that note when the callback source and temporary authority matter mainly because they let the protocol reassign ownership under a coarse aggregate balance check. Predy is the clean seam case: one real callback frame exposed pair-global power, but the enduring accounting lesson is that
same total balances returneddid not prove the same pair ledger still owned them. - Best pair-scoping / pool-ledger exemplar: predy-finance-predypool-callback-locker-cross-pair-liquidity-migration-drain-2024-05-14.
- Best pure cross-provider exemplar: fx-protocol-double-flash-loan-stale-callback-context-victim-position-drain-near-miss-2025-06-17.
- Best cross-account asset-manager exemplar: arcadia-finance-rebalancerspot-swapdata-router-context-spoof-cross-account-drain-2025-07-15.
- Best refund-window exemplar: gmx-v1-arbitrum-orderbook-refund-cross-contract-reentrancy-2025-07-09.
Control surface
-
The leverage sits in temporary capability windows, callback dispatch selectors, who may define actor and target identity during resume, and which balances or accounting marks final payout is allowed to spend.
-
The recurring mistake is subtle: the protocol proves
some trusted callback context exists, quietly promotes that intothis exact provider, actor, target, and stage frame is the one I meant, and then quietly promotes that again intothis frame may still spend this broader payout or settlement authority. -
Sources:
- callback-frame-truth-security-object
- reentrancy
- 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
- gmx-v1-arbitrum-orderbook-refund-cross-contract-reentrancy-2025-07-09
- exactly-protocol-debtmanager-malicious-market-permit-account-rebinding-reentrancy-drain-2023-08-18
- https://www.chainsecurity.com/blog/f-x-protocol-circumventing-access-control-with-a-double-flash-loan-attack
- https://blocksec.com/blog/gmx-incident-cross-contract-reentrancy-bypasses-a-four-year-old-guard
- https://medium.com/@exactly_protocol/exactly-protocol-incident-post-mortem-b4293d97e3ed
-
Last reviewed: 2026-07-28 UTC