SOF Sell-Hook Reserve Manipulation Drains PancakeSwap V2 USDT Liquidity
Exploit Transactions
0xcb5b22d86819b84ef176aee2d6b89f687e74d829560de1bcc63d53fcb2ac68f8Victim Addresses
0xaeB414d0a64DFCA14fd41B28EfC78f437008dF42BSC0x1F3863d274594f25c6203c9272857f0d51B1c010BSCLoss Breakdown
Similar Incidents
STOToken Sell-Hook Reserve Manipulation Drains the STO/WBNB Pancake Pair
47%XDK Sell-Hook Reserve Theft on PancakePair
41%STO Pending-Sell Burn Reserve Manipulation
41%BIGFI Burn Bug Drains PancakeSwap
38%AFX/AHT addLiquidityUsdt abuse drains treasury USDT liquidity
37%AI IPC destroy-sync mechanism drains IPC-USDT pair USDT reserves
37%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.273075721870546397USDT - Value after:
225941.708075549706994529USDT - Net delta:
+225936.434999827836448132USDT - Fees paid in USD: not computed (gas was paid in BNB; native delta
-1053168053167100wei).
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 insideswap(). - The router path
swapExactTokensForTokensSupportingFeeOnTransferTokenstransfers the input token to the pair first, then readsgetReserves()and computesamountInput = balanceOf(pair) - reserveInputbefore callingpair.swap(...). pair.sync()updates stored reserves to match current balances. If a token transfer hook callssync()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
Xsuch thatR - (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
reserveInis arbitrarily small whilebalanceInis 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:
Rbe the pair's stored SOF reserve at the start of the sell.Xbe the attacker sell amount.taxAmount = floor(X / 10)(10% fee), soburnAmount = X - taxAmount.
During the sell transfer to the pair:
- SOF transfers fees out of the seller via
takeFee(from, X). - SOF burns
burnAmountfrom the pair (pair SOF balance decreases byburnAmount). - SOF calls
pair.sync(), updating the stored SOF reserve to the reduced balance, approximatelyR - burnAmount. - SOF then credits the seller->pair transfer of
burnAmount, restoring the pair's SOF balance close toR.
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 SOFburnAmount = X - taxAmount = 787.905580156050321543 SOF
This burnAmount is chosen so that:
R - burnAmount = 1e10wei-units of SOF (0.00000001 SOF), leaving a non-zero dust reserve aftersync().
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 (receiptcontractAddress).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)(selector0xc5ebeaec)repayBorrow(uint256)(selector0x0e752702)
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(selector0x8803dbee). - Path:
USDT (0x55d398...) -> SOF (0xaeB414...). - AmountOut:
991,223.161422615930283861 SOF. - Recipient:
0x3f7cd445f39971c18a4fe303893c6c502b2f68d9(confirmedisExcludedFromFees=true).
Effect on reserves:
- SOF reserve decreases from
992,011.067002781980605404 SOFto787.905580166050321543 SOF.
5.5 Stage 3: Exploit SOF Sell Hook To Drain USDT
Action:
- Call router
swapExactTokensForTokensSupportingFeeOnTransferTokens(selector0x5c11d795). - 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 USDTto0xc4db5bc25b502a366903e93e2229e1386ef9dd3f. - Final pool reserves are essentially drained:
- USDT reserve0:
0.003992900411107843 USDT - SOF reserve1:
787.905580166050321543 SOF
- USDT reserve0:
6. Impact & Losses
- Direct pool depletion: the SOF/USDT PancakeSwap V2 pool (
0x1F3863...c010) lost248,626.248970436629615614 USDT(leaving only0.003992900411107843 USDTin 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
- SOF token:
- 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
- Seed tx metadata and balance deltas: