Calculated from recorded token losses using historical USD prices at the incident time.
0xcb5b22d86819b84ef176aee2d6b89f687e74d829560de1bcc63d53fcb2ac68f80xaeB414d0a64DFCA14fd41B28EfC78f437008dF42BSC0x1F3863d274594f25c6203c9272857f0d51B1c010BSCOn 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):
0x29e5f70ebab2b5b830609e0f2b8a357f2295ebfd5.273075721870546397 USDT225941.708075549706994529 USDT+225936.434999827836448132 USDT-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.
reserve0, reserve1) used for pricing and for the invariant check inside swap().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.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:
0xaeB414d0a64DFCA14fd41B28EfC78f437008dF42): overridden _update(...) sell branch.sync() from inside the sell transfer hook.ACT exploit conditions (as exercised in the incident):
X such that R - (X - floor(X/10)) is a small non-zero dust value.tokenRec() / owner() or another excluded address).Security principles violated:
sync() as part of ERC20 transfer accounting, especially before the transfer completes.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:
reserveIn is arbitrarily small while balanceIn is large, because router/pair pricing and invariant checks assume reserve values are not attacker-controlled mid-flow.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:
takeFee(from, X).burnAmount from the pair (pair SOF balance decreases by burnAmount).pair.sync(), updating the stored SOF reserve to the reduced balance, approximately R - burnAmount.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.
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 SOFThe attacker then sells:
X = 875.450644617833690603 SOFSOF computes:
taxAmount = floor(X / 10) = 87.545064461783369060 SOFburnAmount = X - taxAmount = 787.905580156050321543 SOFThis 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.
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:
0xaeB414d0a64DFCA14fd41B28EfC78f437008dF42) (verified source available).0x1F3863d274594f25c6203c9272857f0d51B1c010) (standard pair logic; verification status not required to validate the exploit).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.
SOF blocks buys unless the recipient is excluded from fees. The attacker exploited this by setting the swap recipient to an excluded address.
Action:
0x10ED43C718714eb63d5aA57B78B54704E256024E) swapTokensForExactTokens (selector 0x8803dbee).USDT (0x55d398...) -> SOF (0xaeB414...).991,223.161422615930283861 SOF.0x3f7cd445f39971c18a4fe303893c6c502b2f68d9 (confirmed isExcludedFromFees=true).Effect on reserves:
992,011.067002781980605404 SOF to 787.905580166050321543 SOF.Action:
swapExactTokensForTokensSupportingFeeOnTransferTokens (selector 0x5c11d795).SOF -> USDT.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:
313,816,344.363195857717111584 USDT to 0xc4db5bc25b502a366903e93e2229e1386ef9dd3f.0.003992900411107843 USDT787.905580166050321543 SOF0x1F3863...c010) lost 248,626.248970436629615614 USDT (leaving only 0.003992900411107843 USDT in reserves).0xcb5b22d86819b84ef176aee2d6b89f687e74d829560de1bcc63d53fcb2ac68f8 (block 81140062; pre-state block 81140061).0xaeB414d0a64DFCA14fd41B28EfC78f437008dF420x55d398326f99059fF775485246999027B31979550x10ED43C718714eb63d5aA57B78B54704E256024E0x1F3863d274594f25c6203c9272857f0d51B1c010artifacts/collector/seed/56/0xcb5b22d86819b84ef176aee2d6b89f687e74d829560de1bcc63d53fcb2ac68f8/metadata.json, artifacts/collector/seed/56/0xcb5b22d86819b84ef176aee2d6b89f687e74d829560de1bcc63d53fcb2ac68f8/balance_diff.jsonartifacts/auditor/iter_0/debug_trace_calltracer.jsonartifacts/auditor/iter_0/receipt.jsonartifacts/auditor/iter_0/pair_state.jsonartifacts/auditor/iter_0/sof_exclusions.jsonartifacts/auditor/iter_0/usdt_transfers.jsonl_update): artifacts/collector/seed/56/0xaeb414d0a64dfca14fd41b28efc78f437008df42/src/SOF/SOF.sol