On April 28, 2026 at 00:00:00 UTC, the T3 JUDAO token on BNB Chain was exploited through a reserve-manipulation flaw in the token’s sell-transfer hook. The attacker used a Moolah flash loan to buy JUDAO from the PancakeSwap V2 JUDAO/USDT pair, then sold almost the maximum amount allowed by JUDAO’s sell check. During the sell transfer, JUDAO removed millions of JUDAO tokens directly from the Pancake pair and called sync() before the attacker’s sold tokens were fully accounted for. This lowered the pair’s JUDAO reserve mid-transaction and let the attacker withdraw excess USDT from the pair.
The attacker repaid the 2,295,723.159642 USDT flash loan and ended with 205,259.490762 USDT plus 36 BNB. The 36 BNB was purchased with 22,613.847147 USDT in the same transaction, so the realized value matches the TenArmor alert: approximately 227,873.337910 USDT (~$227.9K).
JUDAOToken0xf55dff7898930a2d28cdbc39d615b1624ac868880.8.300x5d7b61e91cb59e90f7fae8d0fe2e73976161592f_update(address from, address to, uint256 amount)to == basePair (a sell into the JUDAO/USDT pair)contracts/JUDAO.solThe sell branch in _update() performs reserve-changing side effects against the Pancake pair before completing the user’s sell transfer.
function _update(address from,address to,uint256 amount) internal override {
if(inSwap){
return super._update(from,to,amount);
}
reward(from);
reward(to);
if(to==basePair){
sync(true);
require(startTime>0&&block.timestamp>startTime, "launched");
(uint256 sellFee,bool isBurnPair,uint256 tokenAmount)=getSellFee();
if(amount*10/tokenAmount > 1){
revert("amount K");
}
if(isBurnPair){
uint256 fundAmount = amount/2;
super._update(basePair,address(0xDead),amount-fundAmount);
super._update(basePair,address(this),fundAmount);
ISwapPair(basePair).sync();
accERC20PerPower+=fundAmount*1e36/totalPower;
}
...
super._update(from, address(this), feeAmount+profitFeesAmount);
processFee(feeAmount,profitFeesAmount,sellFee);
return super._update(from,to,amount-feeAmount-profitFeesAmount);
}
}
Key issues:
sync(true) is called at the start of a sell. At the UTC day boundary it can update lastDayReserves, perform daily mining, transfer tokens out of basePair, and call pair.sync().getSellFee() returns isBurnPair == true, the token removes amount JUDAO from basePair (amount/2 to dead, amount/2 to the token contract) and calls pair.sync() before the attacker deposits the sell amount into the pair.amount * 10 / tokenAmount > 1. This permits sells up to just below 20% of tokenAmount, not the apparent 10% cap.processFee() performs an internal JUDAO->USDT swap while the pair reserves are already manipulated.PancakeSwap V2 prices a swap from the pair’s recorded reserves and current token balances. A token should not unexpectedly debit the pair and call sync() during a user transfer to the pair. JUDAO does exactly that:
sync(true), then the isBurnPair branch removes 5.473M JUDAO from the pair and calls pair.sync().pair.swap(2,523,596.497552 USDT, 0, attackerExecutor, "").The exploit was timed at 2026-04-28T00:00:00Z, exactly at a UTC day rollover. This matters because JUDAO’s sync(true) mining path is day-gated by currDays > lastMiningDay; the trace shows it transferring 565,307.959810 JUDAO out of the pair during this first sell of the new day, then the isBurnPair branch transfers another 5,473,557.853503 JUDAO out of the pair.
0x5384...161b creates bootstrap contract 0x3b9b...e432.0x5309...f079 and calls its main function.0x8f73...5d8c.pair.swap() and withdraws 2,523,596.497552 USDT from the JUDAO/USDT pair.Derived from trace_callTracer.json and decoded_calls.json.
EOA 0x5384...161b
CREATE -> bootstrap 0x3b9b...e432
CREATE -> executor 0x5309...f079
STATICCALL JUDAO.basePair() [0x5930919b]
approve USDT/JUDAO to Pancake router and Moolah
CALL executor.main-like selector [0x43436955]
CALL Moolah.flashLoan(USDT, 2,295,723.159642, data) [0xe0232b42]
CALL USDT.transfer(executor, 2,295,723.159642)
CALL executor.onMoolahFlashLoan(...) [0x13a1a562]
CALL PancakeRouter.swapExactTokensForTokens(USDT -> JUDAO)
CALL JUDAO/USDT pair.swap(0, 5,642,843.147941 JUDAO, executor, "")
CALL JUDAO.transfer(executor, 5,642,843.147941)
JUDAO buy hook takes 169,285.294438 JUDAO fee
CALL JUDAO.transfer(pair, 5,473,557.853503)
JUDAO sell hook:
pair.sync() after daily mining removes 565,307.959810 JUDAO
pair.sync() after isBurnPair removes 5,473,557.853503 JUDAO
processFee swaps 389,206.461087 JUDAO for 236,331.524658 USDT
transfers 126,390.017946 USDT to fund pool and other USDT to fee recipients
CALL pair.swap(2,523,596.497552 USDT, 0, executor, "")
CALL USDT.transferFrom(executor, Moolah, 2,295,723.159642)
CALL PancakeRouter.swapTokensForExactETH(36 BNB, max USDT, [USDT,WBNB], attacker EOA)
CALL USDT.transfer(attacker EOA, 205,259.490762)
The key reserve changes are visible from getReserves() outputs and ERC-20 Transfer logs.
| Step | USDT reserve | JUDAO reserve | Notes |
|---|---|---|---|
| Before attack swap | 11,470,690.087303 | 33,908,241.138424 | Pair state before flash-loan-funded buy |
| After attacker buy | 13,766,413.246945 | 28,265,397.990482 | Attacker bought JUDAO with 2.295M USDT |
| After daily mining sync | 13,766,413.246945 | 27,700,090.030673 | sync(true) removed 565,307.959810 JUDAO from pair |
After isBurnPair sync | 13,766,413.246945 | 22,226,532.177170 | Sell hook removed another 5,473,557.853503 JUDAO from pair |
| Before final attacker swap | 13,530,081.722287 | 22,615,738.638257 | Fee processing swapped JUDAO for USDT |
| Final pair swap output | -2,523,596.497552 | +5,198,393.287783 | Attacker withdrew USDT using the manipulated reserve state |
The sell amount was 5,473,557.853503 JUDAO. After the first daily-mining sync, the token reserve used for the cap check was 27,700,090.030673 JUDAO. Therefore:
amount / tokenAmount = 19.7601%amount * 10 / tokenAmount = 1 under Solidity integer divisionamount * 10 / tokenAmount > 1 did not revert.This allowed the attacker to use a near-20% sell amount while still passing the intended size guard.
Primary evidence source: funds_flow.json.
| Asset | Amount | Notes |
|---|---|---|
| USDT | 205,259.490762 | Final transfer from executor to attacker EOA |
| BNB | 36.000000 | Bought via Pancake router and sent to attacker EOA |
| USDT-equivalent value of 36 BNB | 22,613.847147 | The exact USDT spent in-tx to buy 36 BNB |
| Total realized value | 227,873.337910 USDT | Matches the ~$227.9K alert |
Gas cost was 0.0002523222 BNB, negligible relative to the extracted value.
| Address | Asset | Net change |
|---|---|---|
JUDAO/USDT pair 0x5d7b...592f | USDT | -464,204.862568 |
JUDAO/USDT pair 0x5d7b...592f | JUDAO | -6,094,109.212385 |
| Dead address | JUDAO | +3,019,432.906656 |
| JUDAO token contract | JUDAO | +3,074,911.821714 |
| Protocol/fund addresses | USDT | +236,332.096843 total routed by processFee() |
| Moolah flash-loan pool | USDT | 0 net, fully repaid |
Attacker EOA 0x5384...161b | USDT + BNB | +205,259.490762 USDT and +36 BNB |
The attacker profit is lower than the pair’s net USDT decrease because the token’s fee-processing path routed a large part of the extracted USDT to JUDAO-controlled/fund-pool addresses.
0x956e38b8ddb40ba080c8042c685ae52ee5c1b096f1d7f0c4a6c59be3eb4265bd950709742026-04-28T00:00:00Z1,682,1480x5384b34c74024d6563b323351a4bbfa18432161b0x3b9bc53af5012b12b6886a665bb22382211ae4320x530904b5b5ec86cca0528a682614f57f87e7f0790x8f73b65b4caaf64fba2af91cc5d4a2a1318e5d8c0xf55dff7898930a2d28cdbc39d615b1624ac868880x5d7b61e91cb59e90f7fae8d0fe2e73976161592fSelector evidence:
| Selector | Signature | Evidence |
|---|---|---|
0xe0232b42 | flashLoan(address,uint256,bytes) | Moolah verified ABI/source |
0x13a1a562 | onMoolahFlashLoan(uint256,bytes) | Attacker executor recovered code and trace |
0x38ed1739 | swapExactTokensForTokens(uint256,uint256,address[],address,uint256) | Pancake router ABI signature |
0x022c0d9f | swap(uint256,uint256,address,bytes) | Pancake pair ABI signature |
0xfff6cae9 | sync() | Pancake pair ABI signature |
0xd9caed12 | withdraw(address,address,uint256) | Recovered helper contract |
0xaf10939b | fundUSDT(uint256) | Recovered fund-pool implementation |
sync() from an ERC-20 transfer hook during buys or sells.require(amount * 10 <= tokenAmount, "amount K"), and consider using basis-points math.blockTimestampLast as the only control for daily state transitions; state changes at day boundaries should not be triggerable by arbitrary swaps.analysis_plan.json: planner output and contract listtrace_callTracer.json: full call tracetrace_prestateTracer.json: storage diff from the exploit transactiontx.json and receipt.json: transaction metadata and logsfunds_flow.json: decoded transfer flows and net balancesdecoded_calls.json and selectors.json: decoded call tree and selector map0xf55dff7898930a2d28cdbc39d615b1624ac86888/contracts/JUDAO.sol: verified vulnerable source0x530904b5b5ec86cca0528a682614f57f87e7f079/recovered.sol: recovered attacker executor pseudocode