All incidents

SOF Sell-Hook Reserve Manipulation Drains PancakeSwap V2 USDT Liquidity

Share
Feb 14, 2026 08:45 UTCAttackLoss: 248,626.25 USDTManually checked1 exploit txWindow: Atomic
Estimated Impact
248,626.25 USDT
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Feb 14, 2026 08:45 UTC → Feb 14, 2026 08:45 UTC

Exploit Transactions

TX 1BSC
0xcb5b22d86819b84ef176aee2d6b89f687e74d829560de1bcc63d53fcb2ac68f8
Feb 14, 2026 08:45 UTCExplorer

Victim Addresses

0xaeB414d0a64DFCA14fd41B28EfC78f437008dF42BSC
0x1F3863d274594f25c6203c9272857f0d51B1c010BSC

Loss Breakdown

248,626.25USDT

Similar Incidents

Root Cause Analysis

SOF Sell-Hook Reserve Manipulation Drains PancakeSwap V2 USDT Liquidity

1. Incident Overview TL;DR

On BSC (chainid 56) at block 81140062, a single contract-creation transaction (0xcb5b22d86819b84ef176aee2d6b89f687e74d829560de1bcc63d53fcb2ac68f8) drained essentially all USDT from the PancakeSwap V2 SOF/USDT pool (0x1F3863d274594f25c6203c9272857f0d51B1c010). The attacker first moved ~991,223 SOF out of the pool to an address excluded from SOF fees (required because SOF blocks normal buys), then sold a precisely chosen amount of SOF that triggered a buggy SOF sell hook which (a) burns tokens from the pair itself and (b) calls pair.sync() mid-transfer. This forces the pair's stored SOF reserve to an attacker-chosen near-zero value while the actual SOF balance remains high, breaking the AMM's pricing/invariant assumptions and enabling the router/pair math to output nearly all USDT reserves in one swap.

Profit predicate (USDT as the USD reference asset):

  • Adversary address: 0x29e5f70ebab2b5b830609e0f2b8a357f2295ebfd
  • Value before: 5.273075721870546397 USDT
  • Value after: 225941.708075549706994529 USDT
  • Net delta: +225936.434999827836448132 USDT
  • Fees paid in USD: not computed (gas was paid in BNB; native delta -1053168053167100 wei).

Root cause: SOF's overridden ERC20 transfer hook (_update) executes an external call to the PancakeSwap pair (sync()) after burning tokens from the pair but before crediting the seller->pair transfer, creating a reserve/balance mismatch that an attacker can tune via the sold amount.

2. Key Background

  • PancakeSwap V2 pairs are Uniswap V2-style constant-product pools that store reserves (reserve0, reserve1) used for pricing and for the invariant check inside swap().
  • The router path swapExactTokensForTokensSupportingFeeOnTransferTokens transfers the input token to the pair first, then reads getReserves() and computes amountInput = balanceOf(pair) - reserveInput before calling pair.swap(...).
  • pair.sync() updates stored reserves to match current balances. If a token transfer hook calls sync() at an unsafe point (mid-transfer) while also changing the pair's balance, the stored reserves can be forced into states the AMM never expects.
  • SOF blocks buys (transfers from the pair) unless the recipient is excluded from fees, which constrains how the initial SOF reserve depletion can be done.

3. Vulnerability Analysis & Root Cause Summary

This incident is an ATTACK caused by a token-level transfer hook that both (1) mutates AMM pool balances and (2) performs an external call into the AMM (sync()) during transfer accounting. SOF overrides OpenZeppelin ERC20's _update(from,to,amount) to implement fees and anti-bot logic. On sells (transfers to the pair) from a non-excluded address, SOF takes a 10% fee, then burns the net amount from the pair itself (pair -> dead), then immediately calls IUniswapV2Pair(pair).sync() before completing the seller->pair transfer. This allows an attacker to choose a sell amount that drives the pair's stored SOF reserve to an arbitrarily small non-zero dust value, while the post-transfer SOF balance returns close to its original value. The router/pair then uses the manipulated reserves to compute an output amount that is just under the full USDT reserve, draining the pool.

Vulnerable components:

  • SOF token (0xaeB414d0a64DFCA14fd41B28EfC78f437008dF42): overridden _update(...) sell branch.
  • External call to PancakeSwap pair sync() from inside the sell transfer hook.

ACT exploit conditions (as exercised in the incident):

  • A UniswapV2/PancakeV2 pair exists between SOF and a valuable asset (USDT) with non-trivial liquidity.
  • The seller address is not excluded from SOF fees (so the vulnerable sell branch executes).
  • The attacker can choose a sell amount X such that R - (X - floor(X/10)) is a small non-zero dust value.
  • Because SOF blocks buys to non-excluded recipients, the attacker routes the initial buy output to an excluded recipient (e.g., SOF tokenRec() / owner() or another excluded address).

Security principles violated:

  • Do not make external calls to AMM pools from ERC20 transfer hooks.
  • Do not mutate AMM pool balances/reserves (via burns/transfers from the pair) outside the AMM's intended swap/mint/burn flow.
  • Do not call UniswapV2Pair/PancakePair sync() as part of ERC20 transfer accounting, especially before the transfer completes.

4. Detailed Root Cause Analysis

4.1 Vulnerable Code Path

The vulnerable logic is in SOF (0xaeB414d0a64DFCA14fd41B28EfC78f437008dF42), inside its overridden _update hook.

The critical sell branch (exact ordering preserved; simplified):

function _update(address from, address to, uint256 amount) internal override {
    if (isExcludedFromFees[to] || isExcludedFromFees[from]) {
        super._update(from, to, amount);
        return;
    }

    if (_isPairs[from]) { // buy
        revert("not alw buy");
    } else if (_isPairs[to]) { // sell
        uint256 taxAmount = takeFee(from, amount); // returns amount * 10 / 100

        // BUG: burn from the pair, then sync, before crediting seller->pair.
        super._update(_uniswapV2Pair, _destroyAddress, amount - taxAmount);
        IUniswapV2Pair(_uniswapV2Pair).sync();
    }

    amount = amount - taxAmount;
    super._update(from, to, amount);
}

Invariant violated:

  • For a UniswapV2-style pool, stored reserves should only be updated to match actual balances at safe points (mint/burn/swap). Token transfer hooks must not induce reserve states where reserveIn is arbitrarily small while balanceIn is large, because router/pair pricing and invariant checks assume reserve values are not attacker-controlled mid-flow.

4.2 Why This Breaks the AMM

Let:

  • R be the pair's stored SOF reserve at the start of the sell.
  • X be the attacker sell amount.
  • taxAmount = floor(X / 10) (10% fee), so burnAmount = X - taxAmount.

During the sell transfer to the pair:

  1. SOF transfers fees out of the seller via takeFee(from, X).
  2. SOF burns burnAmount from the pair (pair SOF balance decreases by burnAmount).
  3. SOF calls pair.sync(), updating the stored SOF reserve to the reduced balance, approximately R - burnAmount.
  4. SOF then credits the seller->pair transfer of burnAmount, restoring the pair's SOF balance close to R.

At this point, the pair's actual SOF balance is high, but its stored SOF reserve has been forced near zero. The router/pair math that assumes reserves are stable now observes a tiny reserveIn, causing the computed USDT output to approach the full USDT reserve.

4.3 Concrete Incident Numbers (Deterministic)

Public pre-state Sigma_B is BSC mainnet at block 81140061 (immediately before the exploit tx in block 81140062).

After the initial SOF-out setup swap, the pair's SOF reserve is:

  • R = 787.905580166050321543 SOF

The attacker then sells:

  • X = 875.450644617833690603 SOF

SOF computes:

  • taxAmount = floor(X / 10) = 87.545064461783369060 SOF
  • burnAmount = X - taxAmount = 787.905580156050321543 SOF

This burnAmount is chosen so that:

  • R - burnAmount = 1e10 wei-units of SOF (0.00000001 SOF), leaving a non-zero dust reserve after sync().

The non-zero dust ensures the resulting USDT output is slightly less than the full USDT reserve so the pair's internal constraint amountOut < reserveOut can still hold, while still draining essentially all USDT.

5. Adversary Flow Analysis

5.1 ACT Opportunity Definition

  • Target block (B): 81140062.
  • Public pre-state (Sigma_B): BSC mainnet at block 81140061.
  • Transaction sequence: a single adversary-crafted contract-creation transaction signed by an unprivileged EOA (normal inclusion under consensus rules).
  • Success predicate: net USDT profit for the adversary.

5.2 Adversary-Related Accounts

  • 0x29e5f70ebab2b5b830609e0f2b8a357f2295ebfd (EOA): transaction sender and net profit recipient.
  • 0xa82f1941f2b5561241d797a90af6020808041f41 (contract): contract created by the transaction (receipt contractAddress).
  • 0xc4db5bc25b502a366903e93e2229e1386ef9dd3f (contract): address that performs the key Venus borrow/repay and Pancake router calls in the call trace (msg.sender for those calls).

Victim candidates:

  • SOF token (0xaeB414d0a64DFCA14fd41B28EfC78f437008dF42) (verified source available).
  • PancakeSwap V2 SOF/USDT pair (0x1F3863d274594f25c6203c9272857f0d51B1c010) (standard pair logic; verification status not required to validate the exploit).

5.3 Stage 1: Temporary USDT Liquidity Sourcing

Within the same transaction, the adversary contract obtains and later returns large USDT liquidity. The trace shows calls to Venus vUSDT (0xFd5840Cd36d94D7229439859C0112a4185BC0255) using:

  • borrow(uint256) (selector 0xc5ebeaec)
  • repayBorrow(uint256) (selector 0x0e752702)

Observed in-trace amounts correspond to a borrow and repay of 142,386,422.484264147340117021 USDT.

5.4 Stage 2: Drain SOF Reserves From Pair (Buy With Excluded Recipient)

SOF blocks buys unless the recipient is excluded from fees. The attacker exploited this by setting the swap recipient to an excluded address.

Action:

  • Call PancakeSwap V2 router (0x10ED43C718714eb63d5aA57B78B54704E256024E) swapTokensForExactTokens (selector 0x8803dbee).
  • Path: USDT (0x55d398...) -> SOF (0xaeB414...).
  • AmountOut: 991,223.161422615930283861 SOF.
  • Recipient: 0x3f7cd445f39971c18a4fe303893c6c502b2f68d9 (confirmed isExcludedFromFees=true).

Effect on reserves:

  • SOF reserve decreases from 992,011.067002781980605404 SOF to 787.905580166050321543 SOF.

5.5 Stage 3: Exploit SOF Sell Hook To Drain USDT

Action:

  • Call router swapExactTokensForTokensSupportingFeeOnTransferTokens (selector 0x5c11d795).
  • Path: SOF -> USDT.
  • AmountIn: 875.450644617833690603 SOF.

During the SOF transfer to the pair, SOF's sell hook executes the burn+sync-before-credit sequence described above, forcing the stored SOF reserve to dust.

Outcome:

  • The pair transfers 313,816,344.363195857717111584 USDT to 0xc4db5bc25b502a366903e93e2229e1386ef9dd3f.
  • Final pool reserves are essentially drained:
    • USDT reserve0: 0.003992900411107843 USDT
    • SOF reserve1: 787.905580166050321543 SOF

6. Impact & Losses

  • Direct pool depletion: the SOF/USDT PancakeSwap V2 pool (0x1F3863...c010) lost 248,626.248970436629615614 USDT (leaving only 0.003992900411107843 USDT in reserves).
  • Affected parties: liquidity providers in the SOF/USDT pool (loss of USDT principal and price integrity); SOF market liquidity was disrupted.

7. References

  • Exploit transaction (BSC): 0xcb5b22d86819b84ef176aee2d6b89f687e74d829560de1bcc63d53fcb2ac68f8 (block 81140062; pre-state block 81140061).
  • Core contracts:
    • SOF token: 0xaeB414d0a64DFCA14fd41B28EfC78f437008dF42
    • USDT (BEP20): 0x55d398326f99059fF775485246999027B3197955
    • PancakeSwap V2 Router: 0x10ED43C718714eb63d5aA57B78B54704E256024E
    • PancakeSwap V2 Pair (SOF/USDT): 0x1F3863d274594f25c6203c9272857f0d51B1c010
  • Evidence artifacts used:
    • Seed tx metadata and balance deltas: artifacts/collector/seed/56/0xcb5b22d86819b84ef176aee2d6b89f687e74d829560de1bcc63d53fcb2ac68f8/metadata.json, artifacts/collector/seed/56/0xcb5b22d86819b84ef176aee2d6b89f687e74d829560de1bcc63d53fcb2ac68f8/balance_diff.json
    • Call trace: artifacts/auditor/iter_0/debug_trace_calltracer.json
    • Receipt: artifacts/auditor/iter_0/receipt.json
    • Pair state (reserves pre/post): artifacts/auditor/iter_0/pair_state.json
    • SOF excluded addresses snapshot: artifacts/auditor/iter_0/sof_exclusions.json
    • USDT transfer call list (derived): artifacts/auditor/iter_0/usdt_transfers.jsonl
    • SOF verified source (vulnerable _update): artifacts/collector/seed/56/0xaeb414d0a64dfca14fd41b28efc78f437008df42/src/SOF/SOF.sol