Exposed Token-Pull Helper / Approval-Rights Exposure

  • Name: exposed token-pull helper / approval-rights exposure
  • Category: exploit pattern / approval drain / access-control omission / spender-surface exposure
  • Summary: Some protocols legitimately hold ERC-20 spender approvals from user wallets, but then expose their own approval-consuming helper too directly. The bug is not a generic arbitrary-call router, fake callback, or mutable allowlisted address. The protocol already has the right to call transferFrom(), and one public helper forgets to prove who is allowed to make the protocol exercise that right. Once that caller boundary disappears, the approved protocol itself becomes the wallet-drain primitive.
  • What it is:
    • A protocol contract is an approved ERC-20 spender for user wallets because ordinary flows need to pull tokens.
    • The same contract, or a closely related helper/facet on the same spender boundary, exposes a token-pull primitive that accepts caller-shaped from, to, token, amount, or source-mode parameters.
    • The helper forgets to enforce the protocol-layer authorization that should answer who may make this spender act right now?
    • ERC-20 allowance still exists underneath, so the token happily honors the transfer once the protocol contract calls transferFrom().
    • The loss comes from exposing the spender’s own pull primitive, not from steering a generic router into arbitrary external calls.
  • Typical preconditions:
    • Users have already approved the protocol contract, often with large or infinite allowances.
    • A public or otherwise attacker-reachable helper can trigger token pulls from arbitrary approved wallets.
    • The helper does not bind the caller to the wallet owner, an internal allowance system, a signed permit scope, or another protocol-layer authorization object.
    • The token follows normal transferFrom() semantics once called by the approved spender.
    • Victims do not revoke approvals before attackers or defenders race through the same path.
  • Common vulnerable surfaces:
    • public safeTransferFrom / pullToken / claimTokens helpers that were meant to be internal-only
    • convenience functions that support both internal ledger balances and external wallet pulls, but only gate one branch
    • facet or module helpers that inherited spender authority from the main protocol contract without inheriting the same caller restrictions
    • migration, rewards, or settlement helpers that were written as reusable token-movement utilities and later exposed too broadly
    • emergency or admin convenience paths that move user-approved tokens under the assumption that only trusted internal flows will ever reach them
  • Exploit sequence:
    1. The attacker identifies a protocol contract that already holds ERC-20 spender approval from victim wallets.
    2. They find a public helper that can make that contract call transferFrom() with attacker-chosen source and destination semantics.
    3. They call the helper directly, naming an approved victim as the token source and an attacker-controlled recipient as the destination.
    4. The protocol contract invokes the token transfer from its own approved-spender identity.
    5. The token honors the spend because caller == approved protocol, even though the outer caller had no right to trigger that pull.
    6. Attackers repeat across approved wallets until the helper is removed or approvals are revoked.
  • Variants:
    • Public internal-only helper — the clean Bancor shape where a raw token-pull helper was simply left public.
    • Branch-specific authorization omission — one source mode or balance mode is gated correctly while another inherits spender authority without equivalent caller checks, as in Beanstalk’s EXTERNAL path.
    • Shared helper across several token-movement contexts — a reusable primitive written for swaps, rewards, or migration escapes the narrow context it was supposed to live inside.
    • Facet / module exposure drift — the authorization lived elsewhere in the old call graph, but a new public entrypoint reaches the helper without preserving that boundary.

Three spender-helper questions reviewers should not collapse

1. Is this protocol actually an approved spender for the wallet and token in question?

  • This is the raw wallet allowance / permit / delegated spend capacity exists question.
  • It answers whether the token would honor a transferFrom() if the protocol speaks the right calldata.
  • It does not yet answer whether the currently reachable helper or branch may exercise that spender reach.

2. Does this exact helper or branch get to exercise that spender authority?

  • This is the sharper public helper, mode flag, source-wallet meaning, and caller boundary stayed fused question.
  • It is the step Bancor and Beanstalk lost in different ways: the spender authority was real, but the protocol-layer object proving this caller may trigger this wallet pull right now either vanished completely or only survived on sibling branches.
  • A system can have a legitimate approved spender and still fail here because a public utility, EXTERNAL mode, migration branch, or facet-added entrypoint silently reinterprets spender reach as public caller authority.

3. Even if the helper path is authentic, how much spend authority should it still have right now?

  • This is the which source wallets, recipients, assets, amount ranges, and reuse budget survive once this helper is live question.

  • Healthy design should not treat the helper is authentic as equivalent to the helper may now pull from any approved wallet toward any recipient under ordinary standing-allowance budget.

  • The important control is not only there is a caller check somewhere; it is the surviving helper path carries only the wallet, token, recipient, and amount authority that exact flow still deserves.

  • Boundary tests that keep this pattern distinct:

    • Classify it here when the decisive bug is the approved spender exposed its own pull primitive without a caller boundary. If no attacker-routed swap path, dynamic adapter, or generic arbitrary-call surface was needed, this note should do the analytical work.
    • Narrow to arbitrary-call token-approval drain when the attacker must steer a generic router, bridge adapter, multicall path, or low-level external call surface into token theft. Transit, Dexible, Revert, Seneca, Socket, Dough, and LI.FI primarily belong there.
    • Narrow to callback-origin authentication failure when the protocol only spends approvals because it trusted a fake AMM callback counterparty. Sushi RouteProcessor2, ParaSwap Augustus V6, and Primitive sit there.
    • Narrow to approval-by-address / mutable-code execution when the route looked allowlisted but the approved address later resolved to different code. Furucombo is the sharp cousin on that boundary.

What sound design looks like

  • The protocol treats wallet approved this spender, this exact helper or branch may exercise that approval, and this surviving helper path still deserves this much wallet / recipient / amount authority right now as three separate security questions.
  • Every helper that can reach transferFrom() is either internal-only or bound to one explicit authorization object such as msg.sender == source wallet, a signed permit, an internal allowance mapping, or a narrowly scoped role.
  • Mixed balance systems do not let external wallet pull inherit weaker authorization than internal ledger move; each branch proves its own caller right explicitly.
  • Reusable token-movement helpers are not published as general-purpose conveniences unless their caller surface is independently safe.
  • Emergency, migration, and rescue helpers that spend approvals are designed as tightly scoped flows rather than as raw pull from arbitrary wallet utilities, and any surviving helper lane should carry a deliberately smaller wallet / recipient / amount budget than ordinary healthy-mode spending.
  • A healthy review can point to one exact object that fuses spender identity, source-wallet meaning, caller authority, and recipient semantics before any token pull occurs. Spender authority truth as a single security object is the sharper positive-design companion when the practical question becomes what exact authorization object should already exist before this contract is allowed to exercise a user's approval at all?

Detection heuristics

  • Enumerate every public or externally reachable function on approved spender contracts that can ultimately call transferFrom().
  • Ask whether those paths authenticate who may cause the spender to act, not just whether the spender has ERC-20 allowance.
  • For protocols with internal ledgers or balance modes, diff authorization across INTERNAL, EXTERNAL, vault, bridge, and migration branches; these bugs often hide in one branch only.
  • Trace reusable token helpers from every public entrypoint, especially in diamonds, modules, or upgrade-added utility facets.
  • Treat any helper with attacker-shaped from, to, token, amount, or mode flags as suspicious if the contract is also a widely approved spender.

Defenses

  • Keep raw token-pull helpers internal unless there is a strong reason they must be externally callable.
  • When external callability is required, bind the helper to explicit protocol-layer authorization: source-wallet self-call, signed permit scope, internal allowance, or tightly scoped role.
  • Separate internal balance accounting from external wallet pull paths so neither can silently inherit authorization assumptions from the other.
  • Prefer exact-amount approvals, time-bounded permits, or scoped approval frameworks over standing infinite allowances.
  • Add tooling and emergency runbooks for rapid allowance revocation because defenders may need to race attackers through the same reachable helper.
  • Review upgrades and facet additions for new public reachability into old token-movement utilities.

Failure-prevention checklist

  • Inventory every contract that is expected to hold user ERC-20 approvals and treat each one as a wallet-spending boundary, not just as application glue.
  • Preserve the full three-step review ladder in design and testing: spender reach exists, this exact helper or branch may exercise it, and that surviving helper lane only gets this narrower wallet / recipient / amount authority should be checked separately rather than collapsed into one vague protocol has allowance conclusion.
  • Keep any primitive that can reach transferFrom() or equivalent token-pull behavior internal by default. External callability should require an explicit, documented reason.
  • For every externally reachable pull path, define the protocol-layer authorization object separately from ERC-20 allowance: who may trigger the pull, from which wallet, to which recipient, for what asset, under what mode, and under what amount limits.
  • Make mixed balance systems prove authorization independently per branch. EXTERNAL, INTERNAL, vault, migration, bridge, and reward paths should not inherit caller assumptions from one another.
  • Avoid generic helpers that accept attacker-shaped from, to, token, amount, or mode parameters unless the function also binds those fields to a narrow signed intent or a tightly scoped role.
  • Gate reusable token-movement utilities at the lowest layer that can still know the real caller right. Do not rely on upstream routing assumptions if the helper itself is reachable through facets, modules, multicalls, or future upgrades.
  • Minimize standing approval blast radius with exact-amount approvals, expiries, permit scopes, or separate spenders for distinct workflows so one exposed helper cannot monetize every historical approval at once.
  • If one rescue, migration, or manual-completion helper must stay live during degraded operation, automatically narrow the source-wallet set, recipient classes, and spend budget instead of preserving ordinary healthy-mode authority by default.
  • Add tests that enumerate all public entrypoints on approved spender contracts and prove none can pull from an arbitrary approved wallet without the intended protocol-layer authorization.
  • Keep incident runbooks ready for allowance-boundary failures: pause paths, revoke front-end prompts, publish spender addresses, and coordinate mass revocation guidance quickly.

Audit prompts

  • Which contracts in the system are widely approved spenders today, and what public or externally reachable functions can make each one call transferFrom()?
  • For every token-pull helper, where is the check that answers who may cause this approved spender to act right now?
  • Once that helper is reachable, what exact source-wallet set, recipient class, asset set, and spend budget is it still allowed to exercise, and how does that authority shrink during rescue or degraded operation?
  • Can an attacker choose from, to, token, amount, or balance mode directly on any path that reaches an approved spender, even if the contract was originally intended to be an internal utility?
  • Do internal-ledger and external-wallet branches enforce equivalent caller authorization, or does one branch silently inherit the spender’s ERC-20 rights without a protocol-layer gate?
  • Have any facets, modules, migration tools, or upgrade-added helpers created new public reachability into old token-movement utilities that were safe only under the previous call graph?
  • If the function is intentionally public, what exact object constrains recipient semantics, spend scope, replay, and caller identity: self-call, signed permit, internal allowance, nonce-bound order, or role?
  • Could a multicall, delegatecall, router wrapper, or callback path reach the same helper without passing through the caller checks reviewers expect from the normal frontend flow?
  • If a user has granted infinite approval, what stops a stranger from turning that dormant allowance into an immediate wallet drain through this helper?
  • Are there emergency or rescue helpers that spend user approvals under the assumption that only trusted operators or internal flows can ever hit them? If so, where is that assumption enforced onchain, and what narrower authority do those helpers retain if they must stay live?
  • How quickly could the team enumerate affected spender addresses, disable reachable helpers, and tell users exactly which approvals to revoke if this surface became exploitable today?

Lessons that should already be standard

  • This contract is an approved spender, this exact helper or branch may exercise that authority, and this surviving helper lane still deserves ordinary wallet-pull authority right now are separate security questions.

  • Approval-backed helpers are wallet-facing attack surfaces even when they were written as protocol-internal conveniences.

  • Mixed accounting systems are dangerous because one well-gated branch can hide another branch that inherits the same spender power without inheriting the same caller checks.

  • Public utility functions rarely stay narrow forever. If a token-pull primitive is useful, some future facet, upgrade, or integration will eventually try to reuse it.

  • Infinite approvals turn authorization slips into race conditions, but the root bug is usually the exposed spender boundary, not user behavior.

  • The safest token-pull helper is the one that never becomes an externally callable helper in the first place.

  • Reviewers should trace spender authority downward to the exact function that triggers transferFrom(), not stop at the statement that users approved this protocol.

  • Emergency response for approval bugs is part of product design. When the protocol is the approved spender, defenders often need to race attackers through the same path, so any surviving rescue helper should already be designed as a smaller-budget lane rather than ordinary spend authority in disguise.

  • Contracts that never custody user assets can still be wallet drainers if they sit on a widely approved spender boundary and expose the wrong trigger surface.

  • High-signal incidents:

    • [[../../../incidents/approvals-wallets-integrations/bancor-bancornetwork-v0-6-public-safetransferfrom-approval-drain-whitehat-2020-06-18|Bancor BancorNetwork v0.6 public safeTransferFrom() approval drain / whitehat rescue (2020-06-18)]] — the clean public internal-only helper on an already-approved spender exemplar.
    • [[../../../incidents/approvals-wallets-integrations/beanstalk-tokenfacet-transfertokenfrom-external-allowance-omission-approval-drain-near-miss-2022-11-14|Beanstalk TokenFacet transferTokenFrom() EXTERNAL-allowance omission / approval-drain near miss (2022-11-14)]] — the branch-specific cousin where internal-balance spends were gated but the externally approved wallet-pull path was not.
  • Why this pattern matters:

    • It isolates a sharper lesson than generic approval drain: approvals are not the bug by themselves, and even broad router freedom is not required. The protocol can turn into a wallet drainer simply by exposing the wrong helper on the wrong spender boundary.
    • It also preserves a useful audit question for mixed-accounting systems: where exactly does protocol-layer authorization stop and raw ERC-20 allowance begin?

Internal linkages

Control surface