JUDAO Sell-Burn Reserve Manipulation
2026-4-27 23:59:13 Author: www.darknavy.org(查看原文) 阅读量:0 收藏

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:

  1. 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().
  2. If 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.
  3. The size check uses integer division: amount * 10 / tokenAmount > 1. This permits sells up to just below 20% of tokenAmount, not the apparent 10% cap.
  4. 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:

  1. The attacker buys JUDAO, making the pair’s reserves approximately 13.766M USDT / 28.265M JUDAO.
  2. The attacker transfers 5.473M JUDAO to the pair to sell.
  3. Before the transfer completes, JUDAO’s sell hook runs sync(true), then the isBurnPair branch removes 5.473M JUDAO from the pair and calls pair.sync().
  4. The pair’s recorded JUDAO reserve falls to approximately 22.226M JUDAO before the attacker’s sell input is credited.
  5. The attacker then completes the sell and directly calls pair.swap(2,523,596.497552 USDT, 0, attackerExecutor, "").
  6. 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

  1. Attacker EOA 0x5384...161b creates bootstrap contract 0x3b9b...e432.
  2. Bootstrap creates executor contract 0x5309...f079 and calls its main function.
  3. Executor flash-loans 2,295,723.159642 USDT from Moolah proxy 0x8f73...5d8c.
  4. In the flash-loan callback, executor swaps the borrowed USDT for JUDAO through PancakeSwap.
  5. Executor receives 5,473,557.853503 JUDAO after JUDAO’s buy-side fee.
  6. Executor transfers that JUDAO to the JUDAO/USDT pair. The JUDAO sell hook removes pair inventory and synchronizes reserves before the sell finishes.
  7. Executor calls pair.swap() and withdraws 2,523,596.497552 USDT from the JUDAO/USDT pair.
  8. Executor repays 2,295,723.159642 USDT to Moolah.
  9. 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.

StepUSDT reserveJUDAO reserveNotes
Before attack swap11,470,690.08730333,908,241.138424Pair state before flash-loan-funded buy
After attacker buy13,766,413.24694528,265,397.990482Attacker bought JUDAO with 2.295M USDT
After daily mining sync13,766,413.24694527,700,090.030673sync(true) removed 565,307.959810 JUDAO from pair
After isBurnPair sync13,766,413.24694522,226,532.177170Sell hook removed another 5,473,557.853503 JUDAO from pair
Before final attacker swap13,530,081.72228722,615,738.638257Fee processing swapped JUDAO for USDT
Final pair swap output-2,523,596.497552+5,198,393.287783Attacker 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 division
  • The check amount * 10 / tokenAmount > 1 did 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

AssetAmountNotes
USDT205,259.490762Final transfer from executor to attacker EOA
BNB36.000000Bought via Pancake router and sent to attacker EOA
USDT-equivalent value of 36 BNB22,613.847147The exact USDT spent in-tx to buy 36 BNB
Total realized value227,873.337910 USDTMatches the ~$227.9K alert

Gas cost was 0.0002523222 BNB, negligible relative to the extracted value.

Pool and Protocol Balance Changes

AddressAssetNet change
JUDAO/USDT pair 0x5d7b...592fUSDT-464,204.862568
JUDAO/USDT pair 0x5d7b...592fJUDAO-6,094,109.212385
Dead addressJUDAO+3,019,432.906656
JUDAO token contractJUDAO+3,074,911.821714
Protocol/fund addressesUSDT+236,332.096843 total routed by processFee()
Moolah flash-loan poolUSDT0 net, fully repaid
Attacker EOA 0x5384...161bUSDT + 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:

SelectorSignatureEvidence
0xe0232b42flashLoan(address,uint256,bytes)Moolah verified ABI/source
0x13a1a562onMoolahFlashLoan(uint256,bytes)Attacker executor recovered code and trace
0x38ed1739swapExactTokensForTokens(uint256,uint256,address[],address,uint256)Pancake router ABI signature
0x022c0d9fswap(uint256,uint256,address,bytes)Pancake pair ABI signature
0xfff6cae9sync()Pancake pair ABI signature
0xd9caed12withdraw(address,address,uint256)Recovered helper contract
0xaf10939bfundUSDT(uint256)Recovered fund-pool implementation
  1. Do not transfer tokens out of AMM pairs or call sync() from an ERC-20 transfer hook during buys or sells.
  2. 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.
  3. Fix the sell-size guard to compare before division, for example require(amount * 10 <= tokenAmount, "amount K"), and consider using basis-points math.
  4. Avoid using current pair reserves and blockTimestampLast as the only control for daily state transitions; state changes at day boundaries should not be triggerable by arbitrary swaps.
  5. 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 list
  • trace_callTracer.json: full call trace
  • trace_prestateTracer.json: storage diff from the exploit transaction
  • tx.json and receipt.json: transaction metadata and logs
  • funds_flow.json: decoded transfer flows and net balances
  • decoded_calls.json and selectors.json: decoded call tree and selector map
  • 0xf55dff7898930a2d28cdbc39d615b1624ac86888/contracts/JUDAO.sol: verified vulnerable source
  • 0x530904b5b5ec86cca0528a682614f57f87e7f079/recovered.sol: recovered attacker executor pseudocode

文章来源: https://www.darknavy.org/web3/exploits/judao-sell-burn-reserve-manipulation/
如有侵权请联系:admin#unsafe.sh