0xb6a9055e3ce7f006391760fbbcc4e4bc8df8228dc47a8bb4ff657370ccc492560x30bea8ce5cd1ba592eb13fccd8973945dc8555c5BSC0x086ecf61469c741a6f97d80f2f43342af3dbdb9bBSCOn BSC block 44857311, transaction 0xb6a9055e3ce7f006391760fbbcc4e4bc8df8228dc47a8bb4ff657370ccc49256 used a public Pancake V3 flash loan and public PancakeSwap liquidity to extract 11204.841801912892665969 USDT from the JHY/USDT pool. The attacker borrowed 25,000 USDT, bought JHY, added liquidity, repeatedly removed liquidity while JHY pair-originated transfers were under-delivered, sold back into the distorted pool, repaid 25,012.5 USDT to the flash source, and transferred the remaining USDT profit to the attacker EOA.
The root cause is a token-side accounting flaw in JHYToken. Its _transfer logic applies the combined sellLP + sellDead = 3% haircut even when the sender is the PancakePair. PancakePair therefore requests nominal JHY outputs during swap and burn, but recipients receive less than the nominal amount. That breaks PancakePair's exact-transfer assumption, strands JHY in the pair, distorts reserves, and lets an unprivileged attacker extract USDT.
The public components involved are JHYToken at 0x30bea8ce5cd1ba592eb13fccd8973945dc8555c5, the JHY/USDT PancakePair at 0x086ecf61469c741a6f97d80f2f43342af3dbdb9b, PancakeRouter at 0x10ed43c718714eb63d5aa57b78b54704e256024e, and the Pancake V3 flash pool at 0x36696169c63e42cd08ce11f5deebbcebae652050. Seed metadata shows the transaction sender is EOA , calling helper contract , with intermediary participating in the flash-loan callback path.
0x00000000dd0412366388639b1101544fff2dce8d0x802a389072c4310cf78a2e654fa50fac8bdc1a550xAeee14beAac31e7c7c03720f1b173a3Fe110664dPancakePair assumes pair-originated transfers are exact. In burn, it computes amount0 and amount1, transfers those exact nominal amounts, then refreshes balances and reserves. In swap, it similarly transfers the requested amount0Out or amount1Out before deriving the observed inputs from post-transfer balances. This accounting model is safe only if pair-originated transfers deliver exactly what PancakePair asked to send.
JHYToken is fee-on-transfer, but its implementation is inconsistent across paths. On sells into the pair (to == uniswapPair), the token explicitly transfers the dead-wallet and dividend portions before sending the reduced amount to the pair. On buys from the pair (from == uniswapPair), it still reduces the transfer amount by 3% but does not move that missing 3% elsewhere first. The pair therefore under-delivers outputs while still running AMM math on the nominal output value.
The vulnerability class is an AMM integration bug caused by incorrect transfer-tax treatment on pair-originated outputs. JHYToken violates PancakePair's core transfer invariant by applying the output haircut to transfers sent from the pair. The decisive logic is in JHYToken _transfer, where non-exempt transfers fall through to amount = amount.sub(amount.mul(sellLP+sellDead).div(100)); super._transfer(from, to, amount); even after the from == uniswapPair branch. Unlike the sell-to-pair path, the buy-from-pair path does not debit the sender for the missing 3% via separate transfers.
PancakePair does not compensate for this behavior. Its swap and burn functions both compute nominal outputs and then trust _safeTransfer to deliver those amounts exactly. When JHY instead delivers only 97% of the nominal output, the pair's balance changes no longer match the transfer values that its AMM accounting used.
The broken invariant is: when PancakePair sends tokens out during swap or burn, the amount that actually leaves the pair must equal the nominal output amount used by the pair's accounting. The code-level breakpoint is JHYToken _transfer around lines 968-981, especially the unconditional haircut at lines 980-981. That mismatch leaves stranded JHY in the pair, shifts the JHY/USDT reserve ratio, and creates a profitable ACT path using only public liquidity.
The verified JHYToken source shows the fault directly:
if (to == uniswapPair) {
super._transfer(from, _deadWalletAddress, amount.mul(sellDead).div(100));
super._transfer(from, address(dividendLPTracker), amount.mul(sellLP).div(100));
}
if (from == uniswapPair) {
try TokenDividendTracker(dividendLPTracker).setBalance(payable(to), IERC20(uniswapPair).balanceOf(address(to))) {} catch {}
}
amount = amount.sub(amount.mul(sellLP + sellDead).div(100));
super._transfer(from, to, amount);
For sells into the pair, the fee transfers happen explicitly before the reduced transfer to the pair. For pair-originated transfers, no such compensating transfer occurs, yet the amount is still reduced. The recipient receives only 97% of the nominal amount, while the pair retains the difference.
PancakePair's source shows why this is fatal:
amount0 = liquidity.mul(balance0) / _totalSupply;
amount1 = liquidity.mul(balance1) / _totalSupply;
_safeTransfer(_token0, to, amount0);
_safeTransfer(_token1, to, amount1);
...
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out);
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out);
...
_update(balance0, balance1, _reserve0, _reserve1);
burn and swap both assume the token transfer exactly matches the nominal output amount used by the pair. JHYToken breaks that assumption.
The seed trace captures the mismatch during the initial buy:
PancakePair::swap(138406685857484950372243, 0, 0x802a389072c4310CF78A2e654Fa50FaC8bDC1a55, 0x)
JHYToken::transfer(0x802a389072c4310CF78A2e654Fa50FaC8bDC1a55, 138406685857484950372243)
emit Transfer(from: 0x086Ecf61469c741a6f97D80F2F43342af3dBDB9B, to: 0x802a389072c4310CF78A2e654Fa50FaC8bDC1a55, value: 134254485281760401861076)
PancakePair requests 138406685857484950372243 JHY, but the helper only receives 134254485281760401861076 JHY. That is the exact AMM-accounting break described in the root cause.
The trace then shows repeated PancakePair::burn(...) paths. Each burn sends JHY out from the pair and therefore triggers the same output haircut again. Those repeated under-deliveries strand additional JHY inside the pair while the liquidity withdrawal and swap math continues on nominal output values. The seed balance diff confirms the final reserve skew: the pair loses 11217341801912892665969 raw USDT units and gains 90943493991270018344100 raw JHY units.
This reserve shift is the exploit's value source. After the pair is made richer in JHY and poorer in USDT than correct accounting would allow, selling the remaining JHY back into the distorted pool returns excess USDT. The resulting proceeds are sufficient to repay the flash loan plus fee and still leave a large positive USDT balance for the attacker.
The adversary cluster consists of EOA 0x00000000dd0412366388639b1101544fff2dce8d, helper 0x802a389072c4310cf78a2e654fa50fac8bdc1a55, and intermediary 0xAeee14beAac31e7c7c03720f1b173a3Fe110664d. Their roles are supported by the seed trace and balance diff.
Step 1 is public flash-loan funding. The trace shows BEP20USDT::transfer(..., 25000000000000000000000) into the attacker-controlled path and later BEP20USDT::transfer(..., 25012500000000000000000) back to the flash pool, proving use of a public flash facility and full repayment with fee.
Step 2 is the initial buy. The helper calls PancakeRouter::swapExactTokensForTokensSupportingFeeOnTransferTokens(20000000000000000000000, 0, [USDT, JHY], ...), which triggers the under-delivering pair-originated JHY transfer shown above. The reserve distortion begins at this point because PancakePair accounts for the nominal JHY output while the helper receives less.
Step 3 is reserve distortion through LP operations. The helper adds liquidity with the remaining USDT and its acquired JHY, then repeatedly executes removeLiquidity, which routes into PancakePair::burn(...). Each pair-originated JHY transfer during these burns is under-delivered by JHYToken, so JHY accumulates inside the pair while USDT is withdrawn on nominal AMM terms.
Step 4 is unwind and profit realization. The trace records a first large JHY sale via swapExactTokensForTokensSupportingFeeOnTransferTokens(101964224351023876573954, ...), another burn, then a final sale via swapExactTokensForTokensSupportingFeeOnTransferTokens(130201033802342535095506, ...). After repaying 25012500000000000000000 USDT to the flash pool, the trace records BEP20USDT::transfer(0x00000000dd0412366388639B1101544FFF2dCe8D, 11204841801912892665969), and the balance diff records the same amount as the EOA's net gain.
This is a complete ACT path. It uses only public liquidity, public AMM interfaces, and attacker-deployed helper logic. No privileged permissions, signatures, or private orderflow are required.
The direct economic loss falls on liquidity providers in the JHY/USDT PancakePair. By exploiting the mismatch between nominal AMM outputs and actual JHY delivered, the attacker leaves the pair with less USDT and more JHY than intended, transferring value from the pool to the attacker.
Measured loss from the seed balance diff:
{
"token_symbol": "USDT",
"amount": "11204841801912892665969",
"decimal": 18
}
That raw amount corresponds to 11204.841801912892665969 USDT. The attacker EOA also paid 1315340010000000 wei in gas, but that cost is negligible relative to the positive USDT balance delta. The flash pool is not the victim because it is repaid in full with fees.
0xb6a9055e3ce7f006391760fbbcc4e4bc8df8228dc47a8bb4ff657370ccc49256, including sender, recipient, calldata, and block context.0x30bea8ce5cd1ba592eb13fccd8973945dc8555c5, especially _transfer around lines 968-981.0x086ecf61469c741a6f97d80f2f43342af3dbdb9b, especially burn and swap around lines 437-478.11204841801912892665969 raw USDT, the pair loses 11217341801912892665969 raw USDT, and the pair gains 90943493991270018344100 raw JHY.