Spender Authority Truth as a Single Security Object

  • Name: spender authority truth as a single security object
  • Category: approval-funded settlement reference / wallet-spender control-surface checklist / caller-right binding design note / token-pull authorization reference
  • Summary: ERC-20 approval is only the bottom layer of authority. A healthy protocol still has to answer a second question before every pull: who is allowed to make this approved spender exercise that approval, for which token, from which wallet or delegated user proxy, toward which recipient or route, under which purpose and amount limits, and how many times? This note exists to keep that higher-layer object explicit. The secure design target is not the spender has allowance, but one single approval-exercise object that binds spender identity, source-wallet identity, authorized trigger, spend scope, recipient semantics, mode or branch meaning, and single-use or residual-allowance consumption rules into one thing the protocol can actually verify. It is also useful for preserving a three-step review ladder that teams still collapse too often: this spender surface can reach wallet funds, this exact caller and branch may exercise that reach, and this surviving live lane still deserves this much spend authority under the current mode. Just as importantly, if one migration helper, rescue branch, manual completion path, or other surviving approval-consuming lane must stay live during incident response or degraded mode, that lane should inherit a smaller wallet, destination, and branch budget than healthy-mode spending rather than silently becoming ordinary authority because it is the only path left.
  • Why this note exists:
    • The exploit corpus now has a sharp negative pattern for approval-rights exposure.
    • Bancor and Beanstalk TokenFacet show the same deeper design lesson: wallet approved this spender and this outer caller may make the spender act right now are different security questions.
    • Approval-drain incidents often look superficially like generic router abuse, but some of the cleanest failures happen one layer earlier, when the protocol never defined one safe object for spender-authority exercise at all.

The approval-exercise object a healthy system should keep fused

A spender-approved token pull is safest when the protocol can point to one object that answers all of these together:

  1. Spender identity

    • Which exact contract or module is supposed to spend the approval?
    • If the protocol uses facets, adapters, modules, or delegatecall-heavy composition, which reachable surface is actually consuming the ERC-20 allowance?
  2. Source-wallet identity

    • Which wallet, smart account, DSA, vault, or other user-controlled proxy is permitted to be the token source?
    • Is the source authenticated by msg.sender, by a signed permit, by an internal allowance object, by a vault ownership relation, by a route-local acting-account binding, or by some narrower settlement context?
    • If the protocol can temporarily liberate collateral from a user proxy and then spend from that proxy through its own helper, the source-wallet boundary has to prove which exact acting account is allowed to fund that branch instead of assuming protocol already manages this account is enough.
  3. Authorized trigger

    • Who may cause the spender to act right now?
    • This is the field Bancor-style bugs lost: the token trusted the spender, but the protocol forgot to prove the outer caller.
  4. Spend scope

    • Which token, amount range, and optional time or nonce bounds are being exercised?
    • Standing ERC-20 allowance is only raw capacity; the protocol still needs a narrower action scope for each real pull.
  5. Recipient or route meaning

    • Where is value allowed to go: protocol custody, a named pool, a vault, a migration target, a user-selected recipient, or a route-local settlement bucket?
    • to should not be just an arbitrary calldata field if the protocol’s safe meaning was narrower than that.
  6. Mode / branch meaning

    • Which accounting mode is active: internal balance move, external wallet pull, migration pull, rescue path, reward claim, or settlement helper branch?
    • This is the field Beanstalk-style bugs lost: one branch preserved protocol-layer authorization, the EXTERNAL branch did not.
  7. Consumption semantics

    • Is this action single-use, bounded-use, or open-ended until the wallet revokes allowance?
    • If the protocol means one pull, one route, or one migration, it should consume one protocol-layer authorization object even if the underlying ERC-20 approval remains larger.

Three spender-authority questions reviewers should not collapse

1. Can this spender surface reach wallet funds at all?

  • This is the raw ERC-20 allowance / permit / delegated-spend capability exists question.
  • It answers whether a helper, module, migration path, or settlement surface can make the token honor a wallet-funded transferFrom().
  • It does not yet answer whether the current outer caller or branch is allowed to make that spender use the approval.

2. Does this exact caller and branch get to exercise that reach?

  • This is the narrower source wallet, outer caller, branch meaning, recipient semantics, and spend scope stayed bound question.
  • It is the review step Bancor and Beanstalk TokenFacet lost in different ways: the spender surface was real and approved, but the protocol-layer object proving this caller may trigger this spend path now either vanished entirely or only survived on sibling branches.
  • A system can have a reachable approved spender and still fail here because public helpers, EXTERNAL-mode flags, migration wrappers, or other convenience paths silently reinterpret spender reach as public caller authority.

3. Even if the caller and branch are authentic, how much live spend authority do they deserve right now?

  • This is the wallet scope / recipient freedom / branch reuse / per-window budget under the current operating mode question.
  • Healthy-mode spending, degraded operation, rescue flows, and manual completion paths should not all inherit the same approval-exercise object just because they share one spender surface.
  • The important control is not merely leave one approval-consuming helper alive; it is shrink that helper's live spender authority on purpose when the trust boundary or operating mode is weaker.

What sound design looks like

1. Approval and trigger rights are modeled separately

  • The wallet approved this spender is not enough.
  • A second check must answer why may this outer caller make the spender use that approval now?
  • If a protocol cannot name that second object, it is already too close to Bancor.

2. Every transferFrom()-reaching path proves caller rights explicitly

  • Public reachability should be treated as hostile by default.
  • Helpers that can reach transferFrom() should require one explicit caller-right object: source-wallet self-call, signed authorization, internal allowance, tightly scoped role, or a route-local settlement capability.
  • This remains true when the spend source is a user-owned proxy account rather than an EOA. This connector already manages the user's DSA / vault / smart account is not a substitute for proving which caller may make that helper spend from that account right now.
  • Only trusted flows will call this helper is not a design primitive.

3. Recipient meaning should be derived from protocol state where possible

  • Safe designs do not let the spender authority object collapse into arbitrary from / to / token / amount calldata.
  • If the intended spend is pull into this vault, fund this swap, or collect this migration amount, recipient and route meaning should come from committed protocol state, not only from attacker-shaped inputs.

4. Mixed accounting branches should not share authority by accident

  • Internal ledger movement, external wallet pulls, migration helpers, reward claims, and rescue flows may all move tokens, but they should not inherit each other’s caller assumptions.
  • A branch that spends ERC-20 allowances should authenticate that branch’s own caller rights, even if sibling branches already do.

5. Protocol-layer authorization should usually be narrower than raw ERC-20 allowance

  • Healthy systems often need broader standing wallet approval for UX reasons, but they should still exercise that approval through smaller scoped actions.
  • Permits, nonces, exact amounts, bounded epochs, route-specific intents, and one-shot migration rights are all ways to keep the protocol’s action object narrower than the token’s raw allowance state.

6. Reusable token helpers should be treated like privileged interfaces

  • If a helper can make an approved spender act, it is part of the protocol’s real custody boundary even if it only wraps safeTransferFrom().
  • Utility shape does not make it low risk.

7. Surviving approval-consuming lanes should become smaller lanes

  • If one rescue helper, migration path, emergency withdrawal branch, or manual completion lane must remain live while broader routing or settlement trust is degraded, that lane should lose ordinary healthy-mode wallet reach, recipient freedom, branch reuse, and per-window spend budget.
  • This is the only approval-consuming path we still trust enough to keep running should mean this path now has a smaller approval-exercise object and smaller loss budget, not this path may keep using the old broad spender authority because it is all we have left.

Questions worth asking first

  • Which exact contracts are approved spenders for user wallets, smart accounts, and user-controlled proxy accounts today?
  • Which public or externally reachable selectors can eventually make those spenders call transferFrom()?
  • For each selector, what object proves the outer caller may trigger that spend?
  • If only one rescue helper, migration path, or manual completion lane survives during degraded operation, what exact wallet scope, recipient classes, and spend budget disappear automatically for that lane?
  • Does the protocol derive recipient and route meaning from trusted state, or only accept attacker-shaped calldata?
  • Do internal-balance and external-wallet branches authenticate caller rights symmetrically?
  • When the protocol means one migration, one swap, or one settlement, what protocol-layer object gets consumed to prevent repeated exercise of the same broader ERC-20 allowance?

Red flags

  • The token already checks allowance used as if that answered who may trigger the spend
  • public helpers with attacker-shaped from, to, token, amount, or mode flags on contracts that are widely approved spenders
  • mixed internal / external accounting systems where only one branch checks caller rights
  • rescue, migration, or convenience helpers that can spend approvals because they were assumed to be internal-only
  • facet or module additions that newly expose an old token-pull utility without inheriting the old caller boundary
  • designs where revoking wallet approval is the only remaining defense once a helper becomes reachable

Failure-prevention checklist

  • Keep one protocol-layer approval-exercise object above raw ERC-20 allowance, and make every approval-consuming path prove the same spender identity, source wallet, authorized trigger, spend scope, recipient meaning, active branch meaning, and consumption rule before any transferFrom() executes.
  • Model spender approved and outer caller may trigger that spender now as separate questions everywhere, then fail closed if either answer is missing.
  • Centralize caller-right validation for every approval-consuming helper so new facets, modules, migration paths, rescue flows, or convenience wrappers cannot reach the token-pull primitive without inheriting the same authorization gate.
  • Derive recipient and route meaning from trusted local state, typed intents, or tightly scoped signed objects wherever possible instead of treating attacker-shaped from / to / token / amount calldata as complete authorization.
  • Keep mixed accounting modes separate: internal balance moves, external wallet pulls, migration helpers, reward claims, and emergency paths should each prove their own caller-right object instead of inheriting each other’s assumptions.
  • Treat reusable token-pull helpers as privileged custody-adjacent interfaces during design review, testing, and upgrade review, even when the code only appears to wrap safeTransferFrom().
  • Make protocol-layer authorization narrower than raw wallet approval whenever possible with exact amounts, nonce-bounded permits, route-scoped intents, epoch limits, or one-shot migration rights.
  • Consume or clear the protocol-layer authorization object on success, revert, abandonment, or mode transition so a standing wallet approval cannot silently back a second unrelated spend story later.
  • Preserve the full three-step review ladder during design and testing: spender surface can reach wallet funds, this exact caller and branch may exercise that reach, and this live lane still deserves this much authority under the current mode should be checked separately rather than collapsed into one vague spender is approved conclusion.
  • Pre-commit degraded-mode policy too: if only one helper, rescue branch, migration path, or manual completion lane remains live, automatically narrow wallet scope, recipient reach, branch freedom, and per-window token budget instead of preserving healthy-mode spender authority by default.
  • Fuzz public helpers, mode switches, branch flags, facet additions, and helper reachability regressions as wallet-drain scenarios, not only as protocol-accounting correctness tests.
  • Review upgrades for caller-boundary inheritance, not just business-logic correctness: every new approval-consuming entrypoint should prove it still routes through the same wallet / caller / branch / recipient binding machinery as the old path.

Audit prompts

  • What exact object proves this already-approved spender may exercise allowance for this source wallet right now rather than merely because the wallet once approved the contract?
  • Which public or externally reachable selectors can make the approved spender call transferFrom(), and what narrower caller-right object does each selector require?
  • If only one rescue helper, migration path, or manual completion lane survives during degraded operation, what exact wallet scope, recipient classes, mode freedom, and per-window token budget disappear automatically — and which ones dangerously survive just because the same spender surface is still reachable?
  • Do any helper, migration, rescue, reward, multicall, or facet entrypoints reach the same token-pull primitive without the shared caller-authentication checks firing first?
  • Where does the system distinguish wallet approved this spender from this outer caller may trigger this spend, and can those two questions collapse anywhere?
  • Are recipient semantics and spend purpose derived from trusted state, or can attacker-shaped calldata silently reinterpret a pull as a different branch, recipient, or asset flow?
  • Do internal-balance and external-wallet branches authenticate source wallet and caller rights symmetrically, or does one mode inherit trust it never proves for itself?
  • What protocol-layer object is consumed when the system means one migration, one claim, one settlement, or one pull, and what prevents the same wallet approval from backing repeated exercise later?
  • If ordinary flows degrade to one emergency helper, one rescue path, or one surviving approval-consuming branch, what exact wallet scope, recipient set, and spend budget disappear automatically?
  • Could a new module, facet, or adapter expose an old token-pull helper without inheriting its original caller boundary?
  • What telemetry would reveal spender-authority drift early: new callers exercising old helpers, approval pulls from unexpected wallets, branch/mode distributions changing, or recipient patterns that no longer match intended custody semantics?
  • During upgrade review, what proves every approval-consuming path still binds spender identity, source wallet, outer caller, recipient meaning, and mode semantics as one object instead of reconstructing them ad hoc from calldata?

Lessons that should already be standard

  • ERC-20 allowance is raw spend capacity, not complete authorization. A second protocol-layer object still has to prove who may make the spender use that capacity and for what purpose.
  • The spender is ours is not a sufficient control if any public helper, facet, or mode flag can still cause that spender to speak in the attacker’s transaction language.
  • Approved spender is reachable is not the same thing as this exact caller and branch may use it, and even that is not the same thing as this surviving live lane still deserves ordinary spend authority right now.
  • Approval drains are often one missing caller-boundary check away from ordinary convenience code. Utility shape does not make a token-pull helper low risk.
  • Mixed accounting systems are dangerous when only one branch proves caller rights cleanly. The EXTERNAL or wallet pull branch is usually where latent authority confusion escapes.
  • Recipient semantics belong in the authorization object, not only in calldata. If the safe meaning was pull into protocol custody, arbitrary recipient should never be just another valid interpretation of the same spender privilege.
  • Standing infinite approvals amplify blast radius, but they only become drains when the protocol already failed to keep spender identity, caller rights, source wallet, and spend meaning fused.
  • Upgrades reopen caller boundaries. One new helper, facet, or migration path that skips the shared authorization funnel can turn a familiar spender into a wallet drainer immediately.
  • Healthy spender design should be able to answer one machine-checkable question before every pull: why may this exact caller make this exact approved spender pull from this exact wallet toward this exact purpose right now?
  • Only live approval-consuming path left is not the same thing as this path still deserves ordinary spend authority. Weaker live workflow should imply a smaller approval-exercise object, not continuity at the old wallet and recipient budget.

Negative exemplar cluster

  • Bancor BancorNetwork v0.6 preserves the clean spender approved, helper public, caller boundary missing variant.
  • Beanstalk TokenFacet preserves the mixed-accounting variant where protocol-layer authorization existed in one branch but disappeared in the EXTERNAL wallet-pull branch.
  • Dough Finance preserves the delegated-user-proxy variant where public deleverage helpers temporarily liberated collateral from user DSAs and then spent that proxy-side authority through attacker-shaped downstream calldata.
  • approval-rights exposure is the exploit-family landing page when the negative classification question matters more than the positive design target.

Where this note stops

  • Use arbitrary call token-approval drain when the sharper failure is not the spender exposed its own helper, but a generic router or adapter surface let attackers steer approved spending into arbitrary calls.
  • Use callback-origin authentication failure when approved spending only became dangerous because a fake pool or fake callback counterparty was trusted as settlement authority.
  • Use mutable-code execution when the approved endpoint later resolved to different code, so the problem is code-identity drift rather than missing caller-right binding on a stable spender surface.
  • This note begins once the design question is: what one object should have proved that this approved spender may exercise this allowance, for this purpose, right now?

How to use this note with the corpus

  • Start here when the sharper design question is what should a protocol have bound together before an approved spender was allowed to pull from a wallet?
  • Pair with approval-rights exposure when the incident is already classified and the next question is how to design the spender boundary correctly.
  • Pair with arbitrary call token-approval drain when the spender boundary was not directly exposed, but a more generic execution surface still needed the same narrower approval-exercise object.
  • Pair with approval-funded route authority as a single security object when the spender surface is specifically a route interpreter or callback-driven adapter and the sharper question becomes what route-local object should have bounded allowance-backed execution.
  • Use this note to keep audit focus above the ERC-20 layer: allowance is raw capacity, not complete authorization.

Internal linkages

Control surface