Bancor BancorNetwork v0.6 Public safeTransferFrom() Approval Drain / Whitehat Rescue (2020-06-18)

  • Name: Bancor BancorNetwork v0.6 public safeTransferFrom() approval drain / whitehat rescue
  • Date: 2020-06-18
  • Protocol: Bancor
  • Chain: Ethereum
  • Category: approval drain / access-control failure / exposed token-pull helper / whitehat rescue
  • Loss: Bancor said about $455,349 of at-risk approved funds were moved into a safe wallet by its whitehat response, while two front-running bots extracted about $135,229 before Bancor could finish the rescue
  • Summary: Bancor’s new BancorNetwork v0.6 deployment accidentally exposed a token-pull helper that should only have been reachable from inside the contract’s own conversion flow. Because Bancor routers already relied on ERC-20 allowances from user wallets, making safeTransferFrom(IERC20Token _token, address _from, address _to, uint256 _value) public meant any caller could instruct the Bancor contract to spend tokens from any wallet that had recently approved it. This was not a vault drain or AMM pricing exploit. It was the protocol’s own spender identity turning into a wallet-drain primitive once the approval-consuming helper lost its caller restriction.
  • Trigger / entry point:
    • Bancor deployed a new BancorNetwork v0.6 contract on 2020-06-16 and disclosed the issue on 2020-06-18.
    • The proximate code change was Bancor commit bd4060bf060cc55aaa11af86ec03a14026ee5b3d, which refactored raw transfer / transferFrom calls into inherited TokenHandler helpers such as safeTransferFrom(...).
    • In that deployed code, TokenHandler.sol defined safeApprove, safeTransfer, and safeTransferFrom(address _token, address _from, address _to, uint256 _value) as public, and BancorNetwork inherited those selectors through TokenHolder.
    • The refactor then replaced inline token pulls inside convertFor2, xConvert2, and ensureTransferFrom with calls to those helpers, but never restored the intended contract-only boundary around the helper itself.
    • Users who traded through Bancor between June 16 and June 18 and granted approvals to the affected network contracts were exposed until they revoked those approvals.
    • Bancor’s own incident notice told users to revoke approvals for at least these affected contracts: 0x8dFEB86C7C962577deD19AB2050AC78654feA9F7, 0x5f58058C0eC971492166763c8C22632B583F667f, and 0x923cAb01E6a4639664aa64B76396Eec0ea7d3a5f.
  • Root cause:
    • Bancor’s exchange flow correctly depended on wallet approvals only when the network contract itself was the one deciding when and how to pull tokens.
    • In v0.6, a refactor moved token movement into inherited TokenHandler utilities whose selectors were public, effectively turning a contract-internal primitive into an externally callable ABI surface on the same approved spender contract.
    • That collapsed two distinct authority questions into one: has this wallet approved Bancor as spender? and is this specific caller allowed to make Bancor exercise that approval right now?
    • ERC-20 approvals answer only the first question. Once the helper was public, any external caller could supply _from, _to, and _value and let the Bancor contract truthfully exercise allowances against arbitrary approved wallets.
  • Exploit path:
    1. A user traded through Bancor and approved one of the new v0.6 BancorNetwork contracts as a token spender.
    2. Because safeTransferFrom(...) was publicly reachable, any attacker could call the Bancor contract directly instead of going through a legitimate conversion path.
    3. The attacker supplied the approved token address plus an approved victim wallet as _from and an attacker-controlled recipient as _to.
    4. The Bancor contract then executed the actual ERC-20 transferFrom call from its own approved-spender identity.
    5. Tokens left victim wallets without any need for price manipulation, governance capture, or protocol-custodied TVL.
    6. After discovering the bug, Bancor used the same public helper path in a whitehat rescue, transferring exposed balances into safe wallet 0x29551FC9C39A95d8f93Ad4209944fF05846a05F3 before hostile callers could do the same.
    7. Two arbitrage / bot operators front-ran part of that rescue flow and captured about $135,229 before Bancor sought return of the funds via bug-bounty negotiation.
  • What the attacker needed to be true:
    • The victim had to have granted ERC-20 approval to one of the vulnerable BancorNetwork contracts.
    • The token had to honor ordinary transferFrom semantics once called by an approved spender.
    • The public helper had to preserve the Bancor contract’s spender identity while accepting attacker-chosen _from, _to, and _value arguments.
    • Attackers needed to move before users revoked approvals or Bancor’s whitehat transactions reached the same wallets first.
  • Technical anatomy worth remembering:
    • This was an approval exploit without a generic router or arbitrary-call surface. The protocol accidentally exposed the approval-consuming primitive itself.
    • The vulnerable function did not appear as some bespoke emergency drain method. It was an inherited helper from TokenHandler that the v0.6 refactor started calling from normal swap paths, which meant a utility meant to support internal flow control became a standalone public ABI surface on BancorNetwork.
    • The vulnerable function lived on the same contract that users had been taught to trust as a legitimate swap spender, which meant allowance blast radius converted immediately into wallet-drain risk.
    • The bug is a good reminder that approved spender and authorized caller of spender logic are separate security boundaries. ERC-20 allowances do not authenticate why the spender is calling transferFrom().
    • Bancor’s response also showed the ugly operational side of approval incidents: once the bug existed, defenders and attackers were racing through the same public drain path.
  • Representative addresses and artifacts:
    • Bancor disclosure post date: 2020-06-18
    • Affected approval targets named by Bancor: 0x8dFEB86C7C962577deD19AB2050AC78654feA9F7, 0x5f58058C0eC971492166763c8C22632B583F667f, 0x923cAb01E6a4639664aa64B76396Eec0ea7d3a5f
    • Vulnerable helper signature reflected on public contract pages: safeTransferFrom(IERC20Token _token, address _from, address _to, uint256 _value)
    • Bancor whitehat safe wallet: 0x29551FC9C39A95d8f93Ad4209944fF05846a05F3
  • Response / mitigation:
    • Bancor publicly warned recent users to revoke approvals immediately.
    • The team initiated a whitehat sweep using the same exposed function to move about $455,349 of at-risk funds into a safe wallet.
    • Bancor said it pushed a new network contract after the incident.
    • The front end was changed to remove infinite approvals in favor of exact-amount approvals for conversions.
    • Bancor also added revoke transactions to the user flow so recent traders could clear exposed allowances more quickly.

Three approval-exercise promotions this incident collapsed

  1. A real spender approval already existed

    • Victim wallets had genuinely approved the new BancorNetwork v0.6 contracts as ERC-20 spenders for ordinary conversions.
    • That approval answered only would the token honor transferFrom() if Bancor itself called it? It did not answer who else may cause Bancor to make that call.
  2. That standing spender reach was over-promoted into protocol-valid helper authority

    • The refactor exposed safeTransferFrom(...) as a public inherited helper on the same approved spender contract.
    • Bancor therefore stopped requiring a narrower conversion-intent object and instead treated spender approval exists as if it also meant this caller may trigger Bancor’s pull helper with attacker-chosen _from, _to, and _value.
  3. That helper path still inherited full wallet-release authority

    • Once the helper was reachable, the surviving authority budget was far too broad: any caller could make Bancor spend from any approved wallet toward any recipient the helper accepted.
    • The whitehat race made the collapse explicit. Both attacker and defender were reduced to the same public wallet-pull primitive because the protocol had preserved no narrower live budget above allowance.

The lasting review lesson is that wallet approved Bancor, this exact Bancor helper may exercise that approval, and that helper still deserves arbitrary wallet-release budget right now were three different promotions. The incident paid out because Bancor accidentally fused all three.

Pattern tags

  • exposed-token-pull-helper-approval-rights-exposure
  • Bancor’s already-approved spender identity stayed intact while the contract forgot to prove who may make that spender act
  • Public safeTransferFrom(...) accepted attacker-chosen _from, _to, and _value, so allowance-backed wallet pulls no longer needed any legitimate conversion path
  • The same exposed helper became both the theft primitive and the defender whitehat-rescue primitive because the protocol had no narrower protocol-layer authorization object above ERC-20 allowance
  • The incident therefore belonged to the stripped-down approved protocol exposed its own pull primitive branch, not to later router-style arbitrary-call approval drains

Why the exploit worked

  • Bancor’s security boundary failed one layer above ERC-20. User wallets had approved the BancorNetwork contracts as spenders for legitimate swaps, but the protocol no longer answered the separate question which caller may make Bancor exercise that approval right now?
  • Making safeTransferFrom(...) public preserved Bancor’s trusted spender identity while externalizing the decisive control fields: source wallet, recipient, and amount. Once those fields became attacker-shaped inputs, the token contract still saw a truthful approved spender and honored the pull.
  • No narrower conversion-intent object survived above the allowance. The vulnerable helper did not require a signed order, route-local state, self-call from the source wallet, or any other protocol-layer proof that this transferFrom() belonged to a real swap.
  • The exploit surface was therefore unusually direct: the attacker did not need to steer Bancor through arbitrary downstream calldata or abuse a fake integration. Bancor had already exposed the exact allowance-consuming primitive on the spender itself.
  • The whitehat race made the design failure even clearer. Once the helper was public, both attacker and defender were reduced to the same permissionless drain path, which is exactly what healthy spender-boundary design is supposed to prevent.

Detection and audit pivots

  • Enumerate every public or externally reachable selector on widely approved spender contracts and trace whether it can eventually call transferFrom(). If the answer is yes, review that path as a wallet-drain surface, not as harmless utility code.
  • Diff new deployments and upgrades for helper reachability changes, not only business-logic diffs. A token-pull primitive that was safe while internal-only can become catastrophic as soon as ABI exposure changes.
  • Treat attacker-shaped _from, _to, _token, _value, or source-mode parameters on approved spenders as high-signal review triggers. Those fields need a narrower authorization object than raw allowance.
  • Monitor for unexpected third-party callers invoking approval-consuming helpers or for spender contracts pulling from wallets outside ordinary route semantics. That telemetry is often the first live signal that spender approved has collapsed into spender publicly triggerable.
  • Rehearse the incident path for approval-boundary failures: exact spender-address inventory, revoke guidance, and whether defenders would be forced to race attackers through the same reachable helper.

Failure-prevention checklist

  • Keep raw token-pull helpers like safeTransferFrom(...) internal by default. If a spender-facing utility must be externally reachable, document exactly why and bind it to an explicit protocol-layer authorization object rather than raw allowance alone.
  • Treat every newly deployed spender contract as a wallet-facing security boundary and review its full ABI, not just the intended swap or conversion entrypoints. Bancor’s break was not in the headline conversion flow; it was one mistakenly exposed helper on the approved spender itself.
  • Bind spender identity, authorized caller, source wallet, recipient semantics, token, and amount together before any helper can exercise transferFrom(). wallet approved Bancor should never be enough to infer any caller may now make Bancor pull from that wallet.
  • Run upgrade and deployment diff review specifically for visibility changes (internal/private to public/external, new inherited selectors, newly reachable facets, or proxy-exposed utility methods) on any approval-holding contract.
  • Minimize approval blast radius operationally with exact-amount approvals, expiries, or separate spenders per workflow so one exposure bug cannot monetize every dormant unlimited approval at once.
  • Add tests that simulate a hostile third party calling every public selector on approved spender contracts with attacker-shaped _from, _to, and _value fields. If any test can pull from an arbitrary approved wallet, the contract is already a wallet drainer.
  • Keep spender-address inventory, revoke UX, and emergency communication templates ready before launch. Bancor’s practical mitigation depended on quickly naming the three affected spender addresses and pushing users toward revocation.
  • Rehearse whitehat-rescue policy in advance. If defenders would need to race attackers through the same public drain path, the protocol should already know who can authorize that response, where rescued funds land, and how users are made whole.

Audit prompts

  • Which contracts in this deployment are expected to sit in user allowance tables, and what public or externally reachable selectors can make each one call transferFrom()?
  • Has any helper that used to be internal or effectively internal become ABI-reachable through a new deployment, proxy surface, inherited method, or facet change?
  • For every externally reachable token-pull path, where is the check that answers who may make this approved spender act right now separately from does the wallet approve this spender?
  • Can an arbitrary caller choose _from, _to, _token, _value, or source-mode parameters on a spender-facing helper without also proving route meaning, wallet ownership, or some narrower signed intent?
  • If a spender contract needs emergency or migration helpers, what stops those helpers from becoming general-purpose wallet-drain paths for any account that has ever approved the contract?
  • What monitoring would show that third-party callers are invoking approval-consuming helpers outside ordinary swap / conversion semantics before a full wallet sweep is underway?
  • If the protocol discovered this bug in production, could operators immediately enumerate affected spender addresses, surface revoke guidance, and decide whether a whitehat sweep is safer than waiting for users to revoke?
  • Does the front end still ask for broad or infinite approvals to this spender, and if so, what narrower onchain authorization object prevents that standing approval from becoming generic public spend authority if one helper slips?

Lessons that should already be standard

  • ERC-20 approval is not route intent, user consent for arbitrary pulls, or proof that any caller may wake the spender. It is only permission for the approved contract to spend under the contract’s own remaining rules.
  • ABI exposure changes are custody changes when the contract is a widely approved spender. A helper becoming public can be as dangerous as adding a new withdrawal path to a vault.
  • We do not custody user funds is not meaningful protection if the protocol lives inside user wallets as an approved spender. The wallet is the balance sheet, and the spender boundary is the custody boundary.
  • Approval incidents are operationally ugly because defenders may be forced to use the same exploit path as attackers. That makes revoke UX, spender inventory, and predeclared rescue policy part of the product design rather than postmortem housekeeping.
  • Unlimited approvals magnify losses, but the root bug is usually the protocol forgetting to preserve a narrower caller-and-purpose authorization object above those approvals.
  • The safest approved spender is one whose low-level token-pull machinery never becomes a reusable public utility surface.

Why this incident matters

  • It is a clean early exemplar of approval risk turning real the moment a spender contract loses control over who may trigger its token-pull logic.
  • It also says more plainly what the preserved healthy boundary should have been: one approval-exercise object should have bound spender identity, source wallet, authorized trigger, recipient meaning, and spend scope before Bancor was ever allowed to call transferFrom().
  • The incident sits next to later arbitrary-call approval drains like Transit, Dexible, Socket, Seneca, and LI.FI, but the failure is even more stripped down: Bancor did not need attacker-routed calldata or a facet regression. It simply made the pull primitive public.
  • That makes Bancor useful as the same allowance blast radius, fewer moving parts cousin when comparing approval-drain incidents.

Internal linkages

  • Primary recurring family: exposed-token-pull-helper-approval-rights-exposure — the core failure was that Bancor, an already-approved spender, exposed its own token-pull helper without preserving the caller boundary above ERC-20 allowance.
  • Positive-design companion: spender authority truth as a single security object — the sharper design question here is what one approval-exercise object should have kept spender identity, source wallet, authorized trigger, recipient meaning, and spend scope fused together before Bancor was ever allowed to call transferFrom().
  • Adjacent recurring family: arbitrary-call-token-approval-drain — same approved protocol becomes wallet-drain primitive outcome, but router / arbitrary-call cases add attacker-routed execution freedom rather than exposing the spender’s own helper directly.
  • Adjacent recurring family: frontend-compromise-approval-phishing — both families monetize wallet approvals, but Bancor broke after approval had already been granted rather than by tricking the user into signing a fresh malicious approval prompt.
  • Adjacent recurring family: approval-by-address-mutable-code-execution — both families remind readers that this contract is an approved spender is not enough; the live execution path still has to prove the right code and the right caller context are exercising that authority.

Control surface