JUDAO Sell-Burn Reserve Manipulation
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).
Root Cause
Vulnerable Contract
- Name: T3 JUDAO /
JUDAOToken - Address:
0xf55dff7898930a2d28cdbc39d615b1624ac86888 - Chain: BNB Chain
- Source type: Verified source, Solidity
0.8.30 - Liquidity pool: PancakeSwap V2 JUDAO/USDT pair
0x5d7b61e91cb59e90f7fae8d0fe2e73976161592f
Vulnerable Function
- Function:
_update(address from, address to, uint256 amount) - Trigger: ERC-20 transfer where
to == basePair(a sell into the JUDAO/USDT pair) - File:
contracts/JUDAO.sol
The sell branch in _update() performs reserve-changing side effects against the Pancake pair before completing the user’s sell transfer.
Vulnerable Code
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 updatelastDayReserves, perform daily mining, transfer tokens out ofbasePair, and callpair.sync().- If
getSellFee()returnsisBurnPair == true, the token removesamountJUDAO frombasePair(amount/2to dead,amount/2to the token contract) and callspair.sync()before the attacker deposits the sell amount into the pair. - The size check uses integer division:
amount * 10 / tokenAmount > 1. This permits sells up to just below 20% oftokenAmount, not the apparent 10% cap. processFee()performs an internal JUDAO->USDT swap while the pair reserves are already manipulated.
Why It Is Vulnerable
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:
- The attacker buys JUDAO, making the pair’s reserves approximately 13.766M USDT / 28.265M JUDAO.
- The attacker transfers 5.473M JUDAO to the pair to sell.
- Before the transfer completes, JUDAO’s sell hook runs
sync(true), then theisBurnPairbranch removes 5.473M JUDAO from the pair and callspair.sync(). - The pair’s recorded JUDAO reserve falls to approximately 22.226M JUDAO before the attacker’s sell input is credited.
- The attacker then completes the sell and directly calls
pair.swap(2,523,596.497552 USDT, 0, attackerExecutor, ""). - Because reserves were lowered mid-transfer, the final swap returns much more USDT than a normal sell of the same JUDAO amount should return.
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.
Attack Execution
High-Level Flow
- Attacker EOA
0x5384...161bcreates bootstrap contract0x3b9b...e432. - Bootstrap creates executor contract
0x5309...f079and calls its main function. - Executor flash-loans 2,295,723.159642 USDT from Moolah proxy
0x8f73...5d8c. - In the flash-loan callback, executor swaps the borrowed USDT for JUDAO through PancakeSwap.
- Executor receives 5,473,557.853503 JUDAO after JUDAO’s buy-side fee.
- Executor transfers that JUDAO to the JUDAO/USDT pair. The JUDAO sell hook removes pair inventory and synchronizes reserves before the sell finishes.
- Executor calls
pair.swap()and withdraws 2,523,596.497552 USDT from the JUDAO/USDT pair. - Executor repays 2,295,723.159642 USDT to Moolah.
- Executor swaps 22,613.847147 USDT for exactly 36 BNB and transfers the remaining 205,259.490762 USDT to the attacker EOA.
Detailed Call Trace
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)
Reserve Manipulation Evidence
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 = 1under Solidity integer division- The check
amount * 10 / tokenAmount > 1did not revert.
This allowed the attacker to use a near-20% sell amount while still passing the intended size guard.
Financial Impact
Primary evidence source: funds_flow.json.
Attacker Profit
| 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.
Pool and Protocol Balance Changes
| 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.
Evidence
- Transaction:
0x956e38b8ddb40ba080c8042c685ae52ee5c1b096f1d7f0c4a6c59be3eb4265bd - Block:
95070974 - Timestamp:
2026-04-28T00:00:00Z - Status: Success
- Gas used:
1,682,148 - Attacker EOA:
0x5384b34c74024d6563b323351a4bbfa18432161b - Bootstrap contract:
0x3b9bc53af5012b12b6886a665bb22382211ae432 - Executor contract:
0x530904b5b5ec86cca0528a682614f57f87e7f079 - Flash-loan provider: Moolah proxy
0x8f73b65b4caaf64fba2af91cc5d4a2a1318e5d8c - Vulnerable token: JUDAO
0xf55dff7898930a2d28cdbc39d615b1624ac86888 - Drained pair: PancakeSwap V2 JUDAO/USDT
0x5d7b61e91cb59e90f7fae8d0fe2e73976161592f
Selector 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 |
Remediation
- Do not transfer tokens out of AMM pairs or call
sync()from an ERC-20 transfer hook during buys or sells. - If pair burns/mining rewards are required, execute them in a separate keeper-controlled path with strict rate limits and without depending on the current user’s transfer amount.
- Fix the sell-size guard to compare before division, for example
require(amount * 10 <= tokenAmount, "amount K"), and consider using basis-points math. - Avoid using current pair reserves and
blockTimestampLastas the only control for daily state transitions; state changes at day boundaries should not be triggerable by arbitrary swaps. - Add invariant tests around AMM interactions: a transfer to the pair must not reduce the pair’s recorded token reserve before the transfer amount is added.
Artifacts
analysis_plan.json: planner output and contract listtrace_callTracer.json: full call tracetrace_prestateTracer.json: storage diff from the exploit transactiontx.jsonandreceipt.json: transaction metadata and logsfunds_flow.json: decoded transfer flows and net balancesdecoded_calls.jsonandselectors.json: decoded call tree and selector map0xf55dff7898930a2d28cdbc39d615b1624ac86888/contracts/JUDAO.sol: verified vulnerable source0x530904b5b5ec86cca0528a682614f57f87e7f079/recovered.sol: recovered attacker executor pseudocode