Callback Frame Truth as a Single Security Object
- Name: callback frame truth as a single security object
- Category: callback-systems reference / in-flight execution-authenticity checklist / router-and-helper design note / temporary-capability binding reference
- Summary: Healthy callback-driven systems do not ask themselves only
did a trusted contract call back?They ask a stronger question:does this callback belong to the exact in-flight execution frame we meant to resume?That frame is a single security object: the canonical callback source or provider, the acting account on whose behalf the protocol is operating, the exact target position / order / market / account being touched, the stage-specific privilege window that is supposed to be open, the exact payload or route segment the callback may resume, and the balance bucket, ownership ledger, or payout path that may be mutated before control returns. This note exists to keep that positive design target explicit instead of leaving readers to infer it only by inverting incidents like f(x) Protocol, Predy Finance, GMX V1, or Exactly Protocol. - Why this note exists:
- Several modern callback incidents do not fail because the callback caller was obviously fake; they fail because a real callback inherited stale or overly broad authority from a different in-flight operation.
- A boolean like
inFlashLoan,isLeverageEnabled, orthis helper is currently activeis usually too weak once routers, helpers, or keeper paths compose multiple providers, multiple targets, or multiple stages. - The useful design question is often not
was the callback trusted in isolation?butwhat exact execution frame was this callback supposed to resume, and what locally checkable fields proved that?
The six joints a healthy callback frame should keep fused
-
Canonical callback source
- The protocol needs one exact answer to which lender, router, pool, keeper path, token hook, or bridge adapter is allowed to resume this frame.
msg.sender is a known provideris not enough if multiple providers can share one ambient context bit or one temporary capability window.- This is the joint f(x) lost when a genuine Balancer callback inherited Morpho-framed authority.
-
Acting account / beneficiary identity
- The callback frame must bind who the protocol is acting for: the user, vault, position owner, liquidator, or delegated helper beneficiary.
- Healthy systems do not let a callback inherit a stale
recipient,account, or approval surface from a previous stage or a counterfeit endpoint. - This is the joint that matters when helper contracts can rebound from
msg.senderto a victim account mid-flow, as in Exactly Protocol.
-
Target object identity
- The frame must bind which exact position, order, vault, market, loan, or NFT approval the callback may touch.
- A trusted callback source does not imply authority over every live object the protocol can operate on.
- The important question is
which exact object was this callback expected to settle or mutate, and why only that one?
-
Stage-specific capability window
- The frame must bind what temporary privilege is open right now: flash-loan repayment, keeper order execution, liquidation settlement, deleverage unwind, or refund handling.
- Healthy systems treat that privilege as narrow and stage-local, not as a generic
protocol is currently busybit. - If one module opens the privilege and another module can spend it, both modules still need to agree on the same frame identity and lifecycle. A callback should not be able to reach only one half of a supposedly atomic multi-module transition.
- This is the joint GMX V1 lost when an ETH refund callback arrived while
isLeverageEnabledwas still open and could therefore callVault.increasePosition()directly.
-
Exact callback payload or resume path
- The frame must bind which internal function, route step, or callback payload the external call is allowed to resume.
the callback can self-call into the routeris too broad when the payload still determines acting account, target object, or payout meaning.- A healthy system does not let attacker-shaped calldata pick a fresh privileged internal path merely because some trusted callback is in flight.
-
Post-callback balance and payout provenance
- The frame must bind which balances or accounting deltas belong to this in-flight operation when control returns.
whatever balance the router or vault holds at the endis not a safe payout rule if nested execution could have pulled third-party collateral or corrupted cross-module accounting into the same bucket.- Cross-module accounting marks that later feed price, solvency, or payout logic must also stay frame-atomic. If one callback can mutate
sizein one contract without the pairedaverage price, debt mark, or ownership ledger update in another, the protocol has already let the frame split into incompatible economic stories. - This is the joint f(x) lost when the outer close flow swept all router-held collateral, and the joint GMX lost when GLP AUM trusted split short-accounting state as if it all belonged to one consistent order-execution frame.
Three callback-settlement questions reviewers should not collapse
-
Did a real callback for a live frame arrive?
- This is the familiar first question: is the provider genuine, is the stage open, and is the protocol even supposed to be resuming a callback right now?
- f(x), GMX, and Exactly all show why stopping here is not enough.
-
Did that frame deserve authority over this exact inventory, market, pair, or sub-ledger?
- A genuine callback does not automatically get to touch every pool balance, every victim account, or every live market that shares the same helper.
- Predy is the crisp reminder that
real locker is activeis still weaker thanthis locker may move only this pair's liquidity bucket.
-
Did settlement preserve the same payout meaning and ownership scope, rather than merely returning the same aggregate balances?
The pool ended with the same tokens,the router still looks solvent, orthe outer function can finishis not yet proof that the same actor, pair, position, or ledger still owns the resulting withdrawal right.- The callback frame is still broken if a real callback can quietly migrate value from one internal owner ledger to another and final checks only notice whole-contract conservation.
What sound design looks like
-
One callback source, one actor, one target, one stage, one payload meaning, one payout meaning
- The protocol should be able to answer all six questions explicitly at callback time instead of reconstructing them from ambient state and attacker-shaped calldata.
-
The frame is recorded before external control leaves
- Trusted local state for the in-flight frame should be written before the external call or refund path hands control away.
- If the protocol tries to infer the frame only after callback re-entry has begun, it is already trusting the wrong object.
-
The frame is consumed once
- A callback frame should behave like a single-use capability with a nonce-like lifecycle: open, consume once, close.
- Nested or repeated callbacks should not be able to inherit the same authority unless the protocol intentionally opens a new frame with new committed fields.
- In multi-contract flows,
consume oncehas to mean every contract whose state the frame authenticates consumes the same frame, not that one contract clears its bit while another still accepts the old context.
-
Temporary privilege should be narrower than module-wide liveness
leverage is enabled,flash loan is active, orhelper is executingis usually not the right privilege object.- The safer object is
provider X may call function Y once for actor A, target T, stage S, with payload hash H, and only mutate payout bucket B.
-
Payout logic spends frame-local deltas, not ambient balances
- If the protocol pays out by raw
balanceOf(this)or trusts a cross-module mark that another callback could skew mid-frame, the payout surface is part of the callback-authenticity problem. - Healthy systems separate
assets touched by this framefromassets merely present when the frame ended. - Healthy systems also separate
aggregate balances came backfromthe same pair / position / beneficiary still owns the resulting payout right. - The same rule applies to modular accounting state: if redemption, solvency, or AUM trusts a bundle of values across several contracts, that bundle needs one shared callback-frame story or it is not really frame-local.
- If the protocol pays out by raw
Useful positive design shapes
1. Frame-hash or slot-committed callback state
- Before making the external call, commit one explicit frame object in storage: provider, actor, target, stage, allowed internal selector or payload hash, and payout bucket.
- The callback then proves it is consuming that object, not merely that some callback-capable stage is active.
- This is the callback-side analog of route-hash commitment and timelock action-bundle commitment.
2. Provider-specific callback slots instead of one ambient bit
- If multiple lenders, routers, or callback-capable providers exist, each should have a separate committed frame or a provider-bound discriminator.
some flash loan existsis weaker thanMorpho may now call this exact callback once for position 123 owned by user U.- This is the clean design answer to f(x)-style cross-provider frame confusion.
3. Target and actor written before the callback, never decoded from it
- The callback should not get to redefine which user, which NFT approval, which order, or which market is being acted on.
- Those fields belong in trusted pre-committed state, not in calldata the callback supplies on arrival.
- This is the clean design answer to helper-layer actor rebinding and victim-position substitution.
4. Close capability windows before refunds or user-directed transfers
- If the protocol must issue ETH refunds, token sends, or other user-directed transfers during a privileged path, the temporary privilege should close before those transfers happen whenever possible.
- A refund that occurs while the privileged frame is still open is not a harmless UX step; it is another callback entrypoint inside the privileged window.
- This is the clean design answer to GMX V1-style capability leakage through refund paths.
5. Selector allowlists for internal self-calls from callbacks
- If a callback must dispatch into internal logic, it should do so through a tightly bounded selector set with pre-committed arguments or hashes.
- A generic
address(this).call(userData)inside a trusted callback usually means the protocol is authenticating the wrong object. - Trusted callback origin is not a substitute for trusted callback meaning.
6. Frame-local asset buckets and settlement accounting
- The protocol should track which collateral, debt, refund, or output amount belongs to the current frame, instead of sweeping whatever assets happen to sit in contract balance at the end.
- This matters even when callback origin is real. A real callback can still drag unrelated value into the same contract or desynchronize accounting elsewhere before final payout.
- A safe frame ends by paying the delta it produced, not by emptying the room it happens to be standing in.
Design questions worth asking first
- What exact callback frame is supposed to be live right now?
- Which fields of that frame are committed before external control leaves?
- Could a second provider, a nested callback, or a refund path inherit the same authority without opening a fresh frame?
- Can the callback redefine the acting account, target object, or internal selector through attacker-shaped calldata?
- Does the temporary privilege close before any user-directed transfer, refund, or external hook?
- When the frame ends, does payout come from frame-local accounting or from ambient balances / cross-module state that another callback could have polluted?
Red flags that usually mean the protocol is authenticating the wrong callback object
msg.sender is a trusted flash-loan providerwith no statement about which specific operation it is resuminginFlashLoan == trueorisLeverageEnabled == trueused as if that proved actor, target, and stageonlySelfor router-internal dispatch relied on after an attacker-controlled callback still chooses the self-call payloadthe callback can decode the account or position it needs from userDatawhen those values were never pre-committed locallywe settle by whatever balance is left afterwardused as if nested callbacks cannot pull in third-party valuethe caller is a real keeper / lender / routerused as if that alone proved the in-flight execution frame is the intended one
Failure-prevention checklist
- Commit one explicit callback-frame object before any external call leaves the protocol: provider, actor, target, stage, allowed selector or payload hash, and payout bucket.
- Replace ambient
inFlashLoan,isLeverageEnabled,isExecuting, oronlySelfassumptions with provider-specific, stage-specific, single-use callback capabilities. - Bind actor identity and target object in trusted local state before the callback, and refuse any callback path that tries to source those fields from attacker-controlled calldata on arrival.
- Close temporary privilege windows before refunds, ETH sends, token sends, or other user-directed transfers whenever the path can be structured that way.
- If a callback must self-dispatch, constrain it to a narrow selector allowlist plus pre-committed arguments or a payload hash; do not let the callback choose fresh privileged meaning.
- Settle from frame-local deltas, escrow buckets, or explicitly tracked balance changes rather than
balanceOf(this)or other ambient balances that nested execution can pollute. - Reconcile callback completion on ownership scope as well as token totals.
The same aggregate balances came backis weaker thanthe same actor / pair / position kept the withdrawal or claim right those balances were meant to back. - If several contracts contribute state to the same priced or spendable outcome, bind those writes to one shared frame identity so a callback cannot mutate only part of the economic object the protocol later trusts.
- Consume callback frames exactly once and clear them on both success and failure paths; retries or nested callbacks should need a fresh committed frame.
- Separate provider namespaces so one real callback source cannot inherit authority from a different lender, router, keeper, or helper flow that happened to be active earlier in the transaction.
- Treat refund paths, receiver hooks, permit callbacks, and helper-market callbacks as first-class re-entry surfaces in design and testing, not as harmless UX plumbing.
- Fuzz and adversarially simulate multi-provider nesting, stale-context reuse, counterfeit actor rebinding, and payout-bucket pollution in the same transaction.
Audit prompts
- What exact callback frame does this path believe is live right now, and where is each field of that frame committed before external control leaves?
- Which fields are proven by trusted local state versus re-decoded from callback calldata,
msg.sender, or other attacker-influenced context on return? - Can two providers share one ambient execution bit or one temporary capability window, letting a real callback from provider B inherit authority opened for provider A?
- Can the callback switch actor, target position, beneficiary, order id, or internal selector without violating any locally committed hash or slot?
- Does any refund, ETH send, receiver hook, or helper-side transfer happen while the privileged frame is still open?
- When the path settles, does it spend frame-local deltas or whatever balances and accounting marks happen to exist after nested execution finishes?
- Could the callback leave the protocol whole at aggregate token-count level while silently migrating supplier rights, withdrawal rights, or pair-local ownership to a different internal ledger?
- If one contract opens the callback frame and another contract updates pricing, debt, or share-accounting state during that frame, what proves they are consuming the same operation rather than a leaked partial resume path?
- What clears the frame on revert, partial completion, retry, or keeper failure, and can stale context survive long enough for a second callback to consume it?
- Are there any
onlySelf, router-internal dispatch, or helper callbacks whose real security boundary is actuallytrusted payload meaning, not merelytrusted caller? - If a callback is genuine but arrives at the wrong stage, from the wrong provider, or for the wrong actor/target pair, what exact check fails closed?
- Which production telemetry would show callback-frame drift early: overlapping provider windows, repeated frame consumption attempts, unexpected selector use inside callbacks, or payouts that exceed frame-local deltas?
Lessons that should already be standard
Trusted callback originis not the security property;trusted callback frameis. Real providers can still resume the wrong in-flight operation.- Ambient liveness bits are almost always too weak for modern routers and helpers. If one boolean authenticates many stages or providers, the protocol is already over-trusting context.
- Refunds and user-directed sends are callback surfaces whenever temporary authority is still open. UX plumbing does not sit outside the threat model.
- Actor identity, target identity, and payout meaning belong to pre-committed local state, not to calldata interpreted after re-entry begins.
onlySelfis not a defense if attacker-controlled bytes still decide what the self-call means.- Settlement-by-ambient-balance is callback-frame corruption in disguise: it lets unrelated value that entered during nested execution inherit the authority of the current frame.
Same balances came backis not the same thing asthe same payout meaning survived. Aggregate conservation can still hide stolen pair-local, position-local, or beneficiary-local ownership.- Multi-provider composition is a first-class security problem. A protocol that safely handles one flash-loan or keeper provider can still fail the moment two real providers share one context object.
- Cross-contract modularity is a first-class callback risk too. Once one frame’s meaning is split across several contracts, every mark later trusted for pricing, solvency, or payout needs the same frame identity and closure discipline.
- A callback capability should look more like a single-use signed ticket than like
the router is busy right now.
Negative exemplar cluster: what broken callback-frame truth looks like
- f(x) Protocol is the clean
trusted callback, wrong in-flight flash-loan frameexemplar: a genuine Balancer callback inherited a stale Morpho-flash-loan context bit, attacker-shaped self-call payload, victim-approved NFT, and outer payout bucket. - Predy Finance is the strong
real callback, wrong pair-local ownership scopeexemplar: a legitimate trade callback on a fresh pair inherited pool-global locker authority, migrated liquidity out of an older pair, and passed final checks because aggregate pool balances still matched. - GMX V1 is the strong
refund callback inside still-open temporary capability windowexemplar: the callback source was a real ETH transfer during keeper execution, but the protocol still had not closed the privileged leverage frame or kept short-accounting state atomic across modules. - Exactly Protocol is the helper-layer
actor and target rebound before callbackexemplar: the fake market first became the trust anchor, then the re-entrant callback used that stale acting-account context to drain real victim positions.
How to use this note with the exploit corpus
- Start here when the sharper question is
what exact in-flight execution object should this callback-driven router, helper, or keeper path have authenticated before it resumed? - Use Reentrancy when the primary lesson is still the classic one: control returned before accounting or permissions were final.
- Use this note when the narrower design question becomes
what one callback frame should have stayed bound together instead of being reconstructed from ambient state? - Quick routing rule against the settlement-envelope cluster: if the callback source may even be real but the bug is that actor, target, stage, resume-path, or payout authority leaked across one still-live execution frame, stay here; if the decisive break is instead that a downstream consumer trusted the wrong protocol-authored settlement blob or route envelope, pivot to Settlement envelope truth as a single security object and trusted execution-envelope corruption.
- Pivot to root-of-trust substitution when a counterfeit market, bank, jar, or verifier had already become the trusted root object before the callback-frame question even arose.
- Pair with arbitrary-call token-approval drain when the router/helper did not need stale in-flight context at all; it already exposed attacker-directed authority over approvals.
Internal linkages
- Core exploit-side family: reentrancy.
- Adjacent settlement-context companion: settlement-envelope-truth-security-object.
- Adjacent counterfeit-root-object family: account-confusion-root-of-trust-substitution.
- Adjacent approval-surface family: arbitrary-call-token-approval-drain.
- Best current negative exemplars: f(x) Protocol, Predy Finance, GMX V1, Exactly Protocol.
Control surface
-
The decisive control surface is not just
which callback contract called back. -
It is the whole in-flight execution frame: callback source, acting account, target object, temporary capability window, allowed resume path, and the accounting bucket that final payout is allowed to spend.
-
Callback-driven systems stay healthiest when they authenticate that whole frame once, store it before control leaves, consume it once, and close it before any refund, hook, or nested provider can inherit it.
-
Sources:
- reentrancy
- settlement-envelope-truth-security-object
- fx-protocol-double-flash-loan-stale-callback-context-victim-position-drain-near-miss-2025-06-17
- predy-finance-predypool-callback-locker-cross-pair-liquidity-migration-drain-2024-05-14
- 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-26 UTC