Calculated from recorded token losses using historical USD prices at the incident time.
0x247f4b3dbde9d8ab95c9766588d80f8dae835129225775ebd05a6dd2c69cd79f0xb7a254237e05ccca0a756f75fb78ab2df222911bBSC0xbb652d0f1ebbc2c16632076b1592d45db61a7a68BSCOn BSC block 37272888, transaction 0x247f4b3dbde9d8ab95c9766588d80f8dae835129225775ebd05a6dd2c69cd79f executed a one-transaction anyone-can-take drain against the ZZF / ZONGZI system. The adversary flash-borrowed WBNB, manipulated the ZONGZI/WBNB pool price, triggered ZONGZI's LP-burn-and-sync logic, redeemed through ZZF using the manipulated spot quote, repaid the flash loan, and retained 383.438938729511271045 WBNB. The core root cause is that ZZF pays treasury BNB against a same-transaction manipulable AMM spot quote, while ZONGZI exposes a public reserve-burn path that can ratchet that quote upward before redemption.
ZZF (0xb7a254237e05ccca0a756f75fb78ab2df222911b) is configured as the zongziHolder inside ZONGZI (0xbb652d0f1ebbc2c16632076b1592d45db61a7a68). That linkage matters because ZZF's redemption path ultimately causes ZONGZI to transfer native BNB to ZZF, after which ZZF forwards the payout to the redeemer.
ZONGZI is a fee-on-transfer token with sell-side logic that can execute internal treasury operations when the token contract has accumulated enough inventory. On sells into the ZONGZI/WBNB pair 0xd695c08a4c3b9fc646457ad6b0dc0a3b8f1219fe, the contract may call autozongziLiquidityPairTokens(), which burns part of the pair's ZONGZI reserve and then calls sync(). That reserve destruction changes the spot price without requiring matching WBNB input from the attacker.
The seed transaction also uses a Pancake flash-swap from pair 0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae and standard Pancake router calls through . No privileged key, whitelist, or governance action is required.
0x10ed43c718714eb63d5aa57b78b54704e256024eThe vulnerability is an attack-class economic flaw in the redemption design. ZZF's burnToHolder function computes the payout with uniswapRouter.getAmountsOut(amount, path)[1], which is only a current spot quote from the manipulable ZONGZI/WBNB pool. It then immediately calls ZONGZI.zongziToholder(sender, amount, deserved) and later allows receiveRewards to transfer the quoted native value to the attacker. There is no TWAP, oracle sanity check, redemption cap, or realizable-value bound.
That design is especially dangerous because ZONGZI itself exposes a public price-distorting path. During sells into the pair, autozongziLiquidityPairTokens() transfers pair-held ZONGZI to 0xdead and calls pair.sync(). Burning reserve-side ZONGZI while leaving WBNB unchanged raises the quoted WBNB-per-ZONGZI price that ZZF reads. The attacker therefore uses one public path to manipulate the quote and another public path to monetize that manipulated quote against treasury native BNB.
The violated invariant is: a ZZF burn redemption must never pay more BNB than can be realized for the burned ZONGZI under the same public market state. The concrete breakpoint is ZZF pricing via getAmountsOut and paying through zongziToholder immediately after a same-transaction reserve burn and sync in ZONGZI.
The victim-side pricing and payout path is visible directly in verified ZZF source:
function burnToHolder(uint256 amount,address _invitation) external {
address[] memory path = new address[](2);
path[0] = address(_burnToken);
path[1] = uniswapRouter.WETH();
uint256 deserved = uniswapRouter.getAmountsOut(amount, path)[path.length - 1];
require(payable(address(_burnToken)).balance >= deserved, "not enough balance");
_burnToken.zongziToholder(sender, amount, deserved);
_BurnTokenToDead(sender, amount);
burnFeeRewards(sender, deserved);
}
function receiveRewards(address payable to) external {
uint256 amount = balanceOf(msg.sender).sub(burnAmount[msg.sender]);
to.transfer(amount.mul(10**9));
}
The reserve-destruction branch is visible in verified ZONGZI source:
function autozongziLiquidityPairTokens() internal returns (bool) {
uint256 liquidityPairBalance = this.balanceOf(uniswapPair);
uint256 amountTozongzi = liquidityPairBalance.mul(percentForLPzongzi).div(10000);
if (amountTozongzi > 0) {
super._transfer(uniswapPair, address(0xdead), amountTozongzi);
}
IUniswapV2Pair(uniswapPair).sync();
emit AutoNukeLP();
return true;
}
function zongziToholder(address to,uint256 amount,uint256 balance) external {
require(msg.sender == address(zongziHolder), "only zongzis");
super._transfer(to, address(zongziHolder), amount);
require(payable(address(this)).balance >= balance, "Droped out");
payable(address(zongziHolder)).transfer(balance);
}
The exploit sequence observed in the trace is consistent with that code:
0x16b9...0dae transfers 1904.347826086956521739 WBNB to exploit contract 0x0bd0...22a9.0.1 WBNB, then sells manipulated inventory into the ZONGZI/WBNB pair.AutoNukeLP() and shows a transfer from pair 0xd695...19fe to 0xdead of 28123129811934170810531099681871 ZONGZI followed by Sync(...).1909.299130434782608695 WBNB, and forwards the residual 383.438938729511271045 WBNB to the profit-recipient contract.The balance diff confirms the treasury depletion point. ZONGZI's native balance fell from 1296955539645000000000 wei to 173340668966140046708 wei, a decrease of 1123614870678859953292 wei. The pair's ZONGZI balance also fell from 3750000000000000000000000000000000 to 2911251689459664843316076776072432, confirming reserve destruction in the manipulated pool. These state changes are the protocol-side evidence that the price manipulation was converted into a treasury-funded payout.
The adversary cluster in the seed transaction consists of EOA 0x2c42824ef89d6efa7847d3997266b62599560a26, orchestrator 0x0bd0d9ba4f52db225b265c3cffa7bc4a418d22a9, worker 0x1bbadf2ef1840624ed574239d1855d8a589ac20d, wrap/repayment helper 0x9a1e1ce099c8e7a2c73628a24fa792137dfb768c, and final profit recipient 0x20f62b7dd38fbb5c85a6ffe2733f2f12bdb900c9.
The execution flow is:
flash swap WBNB
-> buy ZONGZI
-> sell to trigger AutoNukeLP + sync
-> buy more ZONGZI at manipulated state
-> call ZZF::burnToHolder
-> call ZZF::receiveRewards
-> wrap native BNB to WBNB
-> repay flash pair
-> keep residual WBNB
The trace records the decisive steps explicitly:
emit AutoNukeLP()
emit Transfer(from: PancakePair, to: 0x000000000000000000000000000000000000dEaD, value: 28123129811934170810531099681871)
emit Sync(reserve0: 1500100000000000000000, reserve1: 3721627511779288603926948857900950)
...
emit Transfer(from: 0x9a1E...768c, to: 0x16b9...0daE, value: 1909299130434782608695)
emit Transfer(from: 0x9a1E...768c, to: 0x20F6...00c9, value: 383438938729511271045)
This is an ACT sequence because every call site is permissionless and reproducible from canonical on-chain state. The attacker does not need privileged access to ZZF, ZONGZI, Pancake router, or the flash-loan pair.
The protocol-side loss realized in the seed transaction is 383.438938729511271045 WBNB of net extracted value before gas accounting at the sender EOA. The root_cause artifact records this as the measurable loss for the incident. The same balance-diff artifact also shows gas cost of 5.946709451998029586 BNB to the submitting EOA, leaving 377.492229277513241459 WBNB net after gas on the attacker side.
The affected assets and components are:
0x247f4b3dbde9d8ab95c9766588d80f8dae835129225775ebd05a6dd2c69cd79f0xb7a254237e05ccca0a756f75fb78ab2df222911b0xbb652d0f1ebbc2c16632076b1592d45db61a7a680xd695c08a4c3b9fc646457ad6b0dc0a3b8f1219fe0x16b9a82891338f9ba80e2d6970fdda79d1eb0daeartifacts/collector/seed/56/0xb7a254237e05ccca0a756f75fb78ab2df222911b/src/Contract.solartifacts/collector/seed/56/0xbb652d0f1ebbc2c16632076b1592d45db61a7a68/src/Contract.solartifacts/collector/seed/56/0x247f4b3dbde9d8ab95c9766588d80f8dae835129225775ebd05a6dd2c69cd79f/trace.cast.log and balance_diff.json