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,349of at-risk approved funds were moved into a safe wallet by its whitehat response, while two front-running bots extracted about$135,229before Bancor could finish the rescue - Summary: Bancor’s new
BancorNetworkv0.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, makingsafeTransferFrom(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
BancorNetworkv0.6 contract on 2020-06-16 and disclosed the issue on 2020-06-18. - The proximate code change was Bancor commit
bd4060bf060cc55aaa11af86ec03a14026ee5b3d, which refactored rawtransfer/transferFromcalls into inheritedTokenHandlerhelpers such assafeTransferFrom(...). - In that deployed code,
TokenHandler.soldefinedsafeApprove,safeTransfer, andsafeTransferFrom(address _token, address _from, address _to, uint256 _value)aspublic, andBancorNetworkinherited those selectors throughTokenHolder. - The refactor then replaced inline token pulls inside
convertFor2,xConvert2, andensureTransferFromwith calls to those helpers, but never restored the intendedcontract-onlyboundary 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, and0x923cAb01E6a4639664aa64B76396Eec0ea7d3a5f.
- Bancor deployed a new
- 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
TokenHandlerutilities whose selectors werepublic, 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?andis 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_valueand let the Bancor contract truthfully exercise allowances against arbitrary approved wallets.
- Exploit path:
- A user traded through Bancor and approved one of the new v0.6
BancorNetworkcontracts as a token spender. - Because
safeTransferFrom(...)was publicly reachable, any attacker could call the Bancor contract directly instead of going through a legitimate conversion path. - The attacker supplied the approved token address plus an approved victim wallet as
_fromand an attacker-controlled recipient as_to. - The Bancor contract then executed the actual ERC-20
transferFromcall from its own approved-spender identity. - Tokens left victim wallets without any need for price manipulation, governance capture, or protocol-custodied TVL.
- After discovering the bug, Bancor used the same public helper path in a whitehat rescue, transferring exposed balances into safe wallet
0x29551FC9C39A95d8f93Ad4209944fF05846a05F3before hostile callers could do the same. - Two arbitrage / bot operators front-ran part of that rescue flow and captured about
$135,229before Bancor sought return of the funds via bug-bounty negotiation.
- A user traded through Bancor and approved one of the new v0.6
- What the attacker needed to be true:
- The victim had to have granted ERC-20 approval to one of the vulnerable
BancorNetworkcontracts. - The token had to honor ordinary
transferFromsemantics 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_valuearguments. - Attackers needed to move before users revoked approvals or Bancor’s whitehat transactions reached the same wallets first.
- The victim had to have granted ERC-20 approval to one of the vulnerable
- 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
TokenHandlerthat 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 onBancorNetwork. - 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 spenderandauthorized caller of spender logicare separate security boundaries. ERC-20 allowances do not authenticate why the spender is callingtransferFrom(). - 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,349of 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
-
A real spender approval already existed
- Victim wallets had genuinely approved the new
BancorNetworkv0.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.
- Victim wallets had genuinely approved the new
-
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 existsas if it also meant this caller may trigger Bancor’s pull helper with attacker-chosen_from,_to, and_value.
- The refactor exposed
-
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 primitivebranch, 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
BancorNetworkcontracts as spenders for legitimate swaps, but the protocol no longer answered the separate questionwhich 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 approvedhas collapsed intospender 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(...)internalby 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 Bancorshould never be enough to inferany caller may now make Bancor pull from that wallet. - Run upgrade and deployment diff review specifically for visibility changes (
internal/privatetopublic/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_valuefields. 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
internalor 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 nowseparately fromdoes 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 fundsis 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 partscousin 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 primitiveoutcome, 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 spenderis not enough; the live execution path still has to prove the right code and the right caller context are exercising that authority.
Control surface
-
The decisive leverage sat in one wrongly exposed helper on the same contract that already held spender rights over user wallets.
-
The real broken boundary was not just
wallet approved Bancor, butwhich caller may make Bancor spend, from which wallet, to which recipient, for what amountas one shared approval-exercise object. -
Once that helper became public, Bancor’s spender identity itself became the exploit surface. The wallet had approved Bancor, but Bancor no longer enforced that only its intended conversion flow could consume that approval.
-
Sources:
- https://blog.bancor.network/bancors-response-to-today-s-smart-contract-vulnerability-dc888c589fe4
- https://github.com/bancorprotocol/contracts-solidity/commit/bd4060bf060cc55aaa11af86ec03a14026ee5b3d
- https://raw.githubusercontent.com/bancorprotocol/contracts-solidity/bd4060bf060cc55aaa11af86ec03a14026ee5b3d/solidity/contracts/utility/TokenHandler.sol
- https://raw.githubusercontent.com/bancorprotocol/contracts-solidity/bd4060bf060cc55aaa11af86ec03a14026ee5b3d/solidity/contracts/BancorNetwork.sol
- https://x.com/Bancor/status/1273512800737001472
- https://etherscan.io/address/0x923cAb01E6a4639664aa64B76396Eec0ea7d3a5f
- https://etherscan.io/address/0x29551FC9C39A95d8f93Ad4209944fF05846a05F3
- https://dl.acm.org/doi/fullHtml/10.1145/3545948.3545963
-
Last reviewed: 2026-08-04 UTC