Spartan Protocol Current-Balance Liquidity-Share Inflation Drain (2021-05-01)

  • Name: Spartan Protocol current-balance liquidity-share inflation drain
  • Date: 2021-05-01
  • Protocol: Spartan Protocol
  • Chain: BNB Chain
  • Category: ambient-balance liquidity-share inflation / LP-burn accounting failure / AMM pool-accounting exploit
  • Loss: about $30.5M from the SPARTA/WBNB pool
  • Summary: Spartan Protocol’s removeLiquidity() path did not redeem LP tokens against a synced reserve snapshot. Instead, calcLiquidityShare() read the pool’s current token balances, which meant anyone could inflate the balances seen by LP-burn math simply by transferring assets into the pool before burning. The attacker used a large PancakeSwap flash swap to buy SPARTA, minted SPT1-WBNB LP tokens, then sent additional SPARTA and WBNB directly into the pool outside the normal liquidity-add path. When those previously minted LP tokens were burned, Spartan treated the attacker-donated balances as if they were already backing the old LP position and paid out far more underlying assets than the original deposit justified. The exploit got even worse because Spartan’s cached reserve accounting stayed stale enough that the same amplification assets could then be added as fresh liquidity and withdrawn again. Repeating that cycle drained most of the pool’s WBNB.
  • Trigger / entry point:
    • Spartan exposed public addLiquidity() and removeLiquidity() flows on its Uniswap-v2-like pools, with math delegated to the upgradeable UTILS contract.
    • The vulnerable share-redemption logic lived in calcLiquidityShare(), which consulted live pool balances instead of the cached pooled amounts that Spartan itself tracked.
    • The main public exploit transaction was 0xb64ae25b0d836c25d115a9368319902c972a0215bd108ae17b1b9617dfb93af8 on BNB Chain.
  • Root cause:
    • Spartan’s liquidity-burn path trusted the pool’s current ERC-20 balances as the amount that existing LP tokens were entitled to redeem.
    • That made unsolicited direct transfers into the pool economically meaningful, even though they had not been committed through the protocol’s own liquidity-accounting path.
    • Spartan also failed to synchronize those live balances back into its cached reserve variables before later liquidity-accounting steps, so the same direct-transfer balances could be reinterpreted as fresh user input on the next addLiquidity() call.
    • The exploitable gap was therefore not just burn reads the wrong number; it was a two-step accounting failure where ambient balances first inflated LP redemption value and were then counted again as new liquidity.
  • Exploit path:
    1. The attacker used a PancakeSwap flash swap to borrow 100,000 WBNB, owing 100,260 WBNB by the end of the transaction.
    2. To avoid Spartan’s liquidity-sensitive fee schedule, the attacker bought SPARTA in several smaller batches, then added roughly 2,536,613.2061 SPARTA plus 11,853.3327 WBNB as liquidity and minted 933,350.9599 SPT1-WBNB LP tokens.
    3. The attacker then bought more SPARTA in additional batches, accumulating about 2,639,121.9774 SPARTA, and transferred that SPARTA plus roughly 21,632.1474 WBNB directly into the pool without using the normal liquidity-add accounting path.
    4. Next, the attacker burned the earlier 933,350.9599 SPT1-WBNB. Because calcLiquidityShare() looked at the pool’s now-inflated live balances, Spartan paid out about 2,538,199.1531 SPARTA and 20,694.0594 WBNB.
    5. That redemption was already profitable in WBNB terms relative to the original step-2 deposit, but Spartan’s stale reserve bookkeeping created a second payout path.
    6. The attacker re-added the amplification assets as liquidity, minting about 1,414,010.1599 more LP tokens, then immediately burned them to withdraw about 2,643,882.0741 SPARTA and 21,555.6973 WBNB.
    7. The cycle was repeated multiple times across the attack sequence, draining roughly 90% of the WBNB from the victim pool before the flash swap was repaid.
  • What the attacker needed to be true:
    • Spartan had to leave direct token transfers economically visible to LP-redemption math instead of isolating redeemable reserves from ambient balances.
    • removeLiquidity() had to price burned LP tokens from current balances rather than from a synchronized reserve snapshot.
    • addLiquidity() had to keep treating the difference between current balances and stale cached reserves as fresh input.
    • The attacker needed enough flash liquidity to make the balance inflation large relative to the pool’s existing size.
    • Spartan’s fee schedule had to be batchable rather than absolute, so the attacker could split swap setup across multiple smaller trades.
  • Technical anatomy worth remembering:
    • This was not an oracle-manipulation exploit. The pool’s own liquidity-share accounting was the false oracle.
    • The decisive mistake was letting someone sent tokens here become equivalent to these tokens now back the LP shares being burned right now.
    • The direct transfers were not pure donations to the pool; because reserve synchronization lagged, the attacker could cash them out once through an old LP position and then again by reintroducing them through addLiquidity().
    • The incident is a sharp AMM cousin to broader ambient-balance attribution bugs: live balances were real, but Spartan assigned them to the wrong economic claim. Put more compactly, balances stayed real but reserve-level provenance did not.
  • Representative transactions and addresses:
    • Main exploit transaction: 0xb64ae25b0d836c25d115a9368319902c972a0215bd108ae17b1b9617dfb93af8
    • Publicly tracked attacker collection wallet: 0x3b6e77722e2bbe97c1cfa337b42c0939aeb83671
    • Initial emergency Utils patch: 0x130859c0419f155d33a3b1cc4d8f5c3c815178717d56cdb685d8e9d45ebc78a0
    • Buggy rollback / re-exposure transaction: 0x004447a82421d9b05fd75eca5239efdc6913d96a01fafb49a1d8ee099ec913a0
    • Final patch deployment: 0xf70a35bf0de33746503002a93714397de2fa4c01c922e9793b2e036e066fca8e
  • Response / mitigation:
    • Spartan deployed an emergency UTILS patch on May 3, 2021 after outside researchers reproduced the still-live exploit path against residual pools.
    • That first patch prevented the demonstrated drain path but also blocked ordinary liquidity removals, so it was not a durable fix.
    • A later contract update briefly reverted to the buggy version, re-exposing remaining funds before the final patch was deployed roughly 40 minutes later.
    • Post-incident response also required accurate pre-exploit snapshots because compensation and recovery depended on reconstructing pool ownership before the drain.

Why this incident matters

  • It is a clean example of LP-share redemption becoming exploitable because a protocol priced burn claims from ambient balances rather than from reserve state it had explicitly recognized.
  • It broadens the exploit corpus beyond lending-vault and reward-accounting provenance bugs into AMM liquidity math: the same balance is up, therefore this claim owns it mistake can live inside pool-share issuance and redemption.
  • It is also a reminder that emergency patching can itself become part of the incident record: Spartan briefly moved from live exploit to temporary fix back to re-exposed before finally landing a stable repair.
  • The sharper preserved healthy-side boundary is that tokens are sitting in the pool right now should not have been treated as these tokens already belong to the LP shares being burned right now. Spartan needed a stronger reserve-adoption boundary than current balance happens to be larger.

Pattern tags:

Why the exploit worked

  • Spartan’s removeLiquidity() path priced LP burns from current ERC-20 balances instead of from a protocol-owned reserve snapshot, so direct transfers could immediately inflate what old LP shares appeared to own.
  • The protocol never forced a clean ambient inventory -> recognized reserve adoption step before that burn math ran.
  • Cached reserve bookkeeping then lagged enough that the same attacker-supplied balances could be reinterpreted again as new liquidity on the next addLiquidity() call.
  • The pool was large enough, and flash liquidity deep enough, that misattributed balances could be converted into real WBNB before operators or patches shut the path down.

Detection and audit pivots

  • Search AMM mint / burn paths for any place where LP entitlement is derived from raw balanceOf(pool) rather than from synchronized reserve variables or an explicitly journaled reserve object.
  • Ask what exact event is allowed to move balances from ambient inventory into recognized pool backing. If the answer is effectively any token transfer before burn, the pool is too trusting.
  • Model the full two-step path, not only the first over-withdrawal: can attacker-added balances improve one burn payout and then later reappear as fresh deposit input because reserve caches never cleanly adopted them?
  • Re-test pool accounting under direct transfers, fee-on-transfer assets, manual sync helpers, and stale-cache conditions; provenance failures in AMMs often hide at the seam between live balance and recognized reserves.
  • Treat emergency patches and rollback procedures as part of the security boundary. If a hotfix can briefly restore the old accounting path, remaining pool inventory is still economically live.

Failure-prevention checklist

  • Burn LP tokens against one canonical recognized-reserve object, not against raw token balances that any outsider can inflate with direct transfers.
  • Keep direct transfers economically inert until one explicit reserve-adoption step decides what they mean. Tokens arrived at the pool should not by itself improve LP redemption value.
  • Make mint, burn, quote, and reserve-sync paths consume the same reserve ledger. The same balance should not help an old LP burn first and then reappear as fresh addLiquidity() input later.
  • Reconcile reserve caches atomically around liquidity removal. If a burn path updates only decremented cached reserves while raw balances remain higher, the protocol is already publishing two incompatible pool states.
  • Add invariants that replay the exact hostile sequence Spartan exposed: mint LP, direct-transfer both assets, burn, re-add the same balances, and try to burn again. The second monetization should fail.
  • Treat sync, cache writers, and accounting helpers as solvency code. If those helpers can be skipped, delayed, or rolled back, they belong inside the same review scope as swap and LP math.
  • Keep emergency patch rollout narrow and monotonic. Hotfix deployment, rollback permissions, and upgrade sequencing should not allow a buggy accounting path to come back online over still-live pool inventory.
  • Stress-test low-liquidity and fee-bypass behavior together. If the attacker can cheaply batch into the setup state, a provenance bug that looks marginal in one swap may still become treasury-scale over several loops.

Audit prompts

  • What exact state do LP tokens redeem against here: current ERC-20 balances, cached reserves, or another journaled reserve object?
  • Can anyone improve a burn payout merely by transferring assets into the pool before removeLiquidity() runs, even if those assets never passed through the official liquidity-add path?
  • After a burn, could the same attacker-supplied balances still appear as fresh actualInput on the next liquidity-add call because cached reserves were only decremented, not fully resynchronized?
  • Which function is the sole authority allowed to adopt ambient pool balances into recognized reserves, and can mint / burn logic bypass that authority by reading raw balances directly?
  • If the protocol supports manual sync-like helpers, fee-on-transfer assets, or upgradeable math modules, do all of those paths preserve one reserve story under direct-transfer stress?
  • Under what conditions can batching around fee logic make it materially cheaper to reach the dangerous state, and was that economic setup tested alongside the accounting invariant?
  • If operators deploy an emergency patch, what exact permissions or upgrade rails could accidentally restore the buggy math while meaningful inventory is still on the line?
  • If a reviewer watched only transfer logs, what evidence would distinguish pool received a donation from pool recognized new redeemable LP backing?

Lessons that should already be standard

  • AMM balances are not self-authenticating reserves. The pool holds more tokens now was never the same thing as existing LP shares earned more backing.
  • LP-burn math can become its own false oracle. A venue does not need an external price feed bug to print withdrawable value if its internal reserve provenance is wrong.
  • Reserve adoption is a first-class control surface. Direct transfers, cache writes, sync helpers, and burn pricing all have to agree on when ambient inventory becomes canonical pool backing.
  • Double-counting is often the real payout multiplier. The first over-withdrawal mattered, but Spartan became devastating because the same balances could then re-enter as fresh liquidity and be monetized again.
  • Upgrade and incident-response paths are part of the exploit surface. A temporary patch that can revert to buggy accounting while reserves remain live is not merely an ops footnote.
  • Flash loans scaled the exploit, but they did not create it. The root bug was that Spartan credited the right balances to the wrong LP claim before reserve accounting had actually adopted them.
  • sync later is not a safety argument when users can already burn, mint, or quote against the unsynced state now.

Internal linkages

  • Core recurring pattern: balance-delta-provenance-failure — Spartan is the AMM ambient balances treated as already-recognized LP backing cousin, where balances stayed real but reserve-level provenance lagged behind what LP-burn math assumed.
  • Positive-design companion: reserve-backed-claim-truth-security-object — the healthier question is what exact reserve object LP shares were supposed to redeem against, and when unsolicited balances should become part of that object at all.
  • Publication / adoption companion: sync-harvest-and-cache-write-authority-discipline — the sharper follow-up is why one balance change became spendable LP-burn truth before Spartan had committed that change through one authoritative reserve-sync path.
  • Adjacent comparison: allbridge-core-bnb-chain-busd-usdt-virtual-balance-withdrawal-mismatch-drain-2023-04-01 — another pool-accounting exploit where LP-style withdrawal math corrupted the next state, though Allbridge’s failure lived in virtual-liability retirement rather than ambient-balance inclusion.
  • Adjacent comparison: balancer-sta-deflationary-token-gulp-pool-drain-2020-06-28 — both incidents let real token balances and economically recognized reserves drift apart, but Balancer mismeasured what arrived from a fee-on-transfer token while Spartan misattributed unsolicited balances as redeemable LP backing.

Control surface