Calculated from recorded token losses using historical USD prices at the incident time.
0x5ef1edb9749af6cec511741225e6d47103e0b647d1e41e08649caaff66942a910xde3595a72f35d587e96d5c7b6f3e6c02ed2900abBSC0xeab0d46682ac707a06aefb0ac72a91a3fd6fe5d1BSCOn BSC block 45561316, attacker EOA 0x09ea8b5e546914746f3dc686ac164486a607fb7b called helper contract 0x3be77a356848cf7220503e62e93dfd0ff3f0074a to execute a single-transaction flashloan-funded drain of the AI IPC IPC/USDT Pancake pair 0xDe3595a72f35d587e96d5C7B6f3E6C02ed2900AB. The helper borrowed USDT from two public DODO pools, ran sixteen manipulated buy/sell iterations against the pair, repaid both loans, and realized 591934203953053577941826 raw USDT units of profit. The pair lost the same USDT amount and also lost 991564846557401299746225 raw IPC units.
The root cause sits in the IPC token contract 0xEAb0d46682Ac707A06aEFB0aC72a91a3Fd6Fe5d1. Its sell path burns IPC directly from the AMM pair via _destroy() and immediately calls sync(), which mutates reserves outside normal swap accounting. Separately, its buy detection relies on _isRemoveLP(pair), which compares live pair USDT balance to reserves during the middle of a pair.swap. By requesting 1 wei of USDT output together with IPC output, the attacker makes the transfer look like LP removal, bypasses the buy fee and transfer lock, and can instantly sell back into the manipulated price.
IPC is a fee-on-transfer token with custom branching inside _transfer(). The token distinguishes buys, sells, ordinary transfers, LP adds, and LP removals by observing the Pancake pair and comparing its live USDT balance against reserve values. It also maintains a sell-side state variable , later consumed by .
destroyNum_destroy()The IPC/USDT pool is the relevant victim venue. Before the exploit, the pre-state already satisfied the conditions needed for a permissionless attack: owner() had been renounced, isOpenSwap() was true, and pool() was fixed to reward pool 0x054525bF471dfBAD447e27B45c763ce6e2B05a78. The pair also held deep liquidity: 591936766071540496160479 raw USDT units and 1353896825896647266100919 raw IPC units.
The pair side matters because Pancake-style pairs transfer outputs before final invariant enforcement. That means token transfer hooks can observe transient balances inside the same swap. IPC's logic incorrectly treats those transient balances as reliable LP-removal evidence.
This is an ATTACK-category ACT exploit against token-side accounting, not a privileged-owner incident. The IPC token violates AMM reserve integrity by burning IPC directly from the pair balance inside _destroy(), calling IUniswapV2Pair(pair).sync(), and then minting twice that amount to the external reward pool. That reserve mutation is not paired with an equivalent quote-asset inflow, so it artificially raises the token price seen by the next sell.
The second bug is the buy-lock bypass. In _transfer(), the buy branch only executes when sender == pair && !_isRemoveLP(pair). _isRemoveLP(pair) returns true whenever the pair's current USDT balance is below reserves. During a direct pair.swap(1, ipcOut, attacker, data) call, the pair temporarily sends out 1 wei USDT before the IPC transfer finishes, so the live USDT balance is reserve - 1. IPC therefore misclassifies the transfer as LP removal, skips the buy fee, and never writes transferTime[recipient], so the attacker can sell immediately.
These two behaviors compose cleanly. Each sell consumes the accumulated destroyNum, burns IPC directly from the pair, syncs the lower reserve, and extracts USDT at an inflated price. Repeating the manipulated buy-immediate-sell loop sixteen times drains almost all pair USDT while minting a large amount of IPC to the configured pool.
The relevant victim-side code is in the collected IPC source:
function _transfer(address sender, address recipient, uint256 amount) internal {
...
if (sender == pair && !_isRemoveLP(pair) && recipient != address(this)) {
fee = amount * MARKET_TAX / 1000;
transferTime[recipient] = block.timestamp;
_buy(fee);
} else if (recipient == pair && sender != address(this)) {
if (_isAddLP(pair)) {
lastSellIsAdd = true;
} else {
if (block.timestamp < transferTime[sender] + TRANSFER_LOCK) revert TransferTimeLock();
fee = amount * (MARKET_TAX + PUBLISH_TAX) / 1000;
_destroy(destroyNum);
destroyNum += (amount - fee) / 2;
lastDestroyNum = (amount - fee) / 2;
_sell(fee);
}
}
...
}
function _destroy(uint256 burnNum) internal {
if (burnNum < 1) return;
address pair = IUniswapV2Factory(SWAP_V2_FACTORY).getPair(USDT, address(this));
...
balances[pair] -= burnNum;
balances[address(0)] += burnNum;
IUniswapV2Pair(pair).sync();
...
balances[pool] += burnNum * 2;
}
function _isRemoveLP(address pair) internal view returns(bool) {
...
return IERC20(USDT).balanceOf(pair) < USDTReserve;
}
Origin: collected IPC token source.
The invariant that should hold is straightforward: the pair's balances should only move according to genuine swap or LP flows, and a normal buy should not be able to skip buy-fee and transfer-lock logic. IPC breaks both invariants. _destroy() directly subtracts from balances[pair] and immediately sync()s the pair before the current sell settles. _isRemoveLP() uses a transient mid-swap balance observation as if it were a final LP state.
The exploit transaction 0x5ef1edb9749af6cec511741225e6d47103e0b647d1e41e08649caaff66942a91 proceeds from the pre-state at block 45561315, where IPC is already open for trading and ownership is address(0). The helper contract first receives two public DODO flashloans. It then performs sixteen direct pair swaps that ask for 1 wei USDT plus IPC output, receives IPC without triggering the normal buy path, and immediately routes the IPC back through PancakeRouter for USDT.
That pattern is visible in the collector trace: repeated direct calls from the helper into pair 0xde3595..., repeated router calls via 0x10ed43..., and repeated token-side pair interactions around _destroy() and sync(). The balance-diff artifact then confirms the end state:
{
"token": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0xde3595a72f35d587e96d5c7b6f3e6c02ed2900ab",
"before": "591936766071540496160479",
"after": "2562118486918218653",
"delta": "-591934203953053577941826"
}
{
"token": "0xeab0d46682ac707a06aefb0ac72a91a3fd6fe5d1",
"holder": "0x054525bf471dfbad447e27b45c763ce6e2b05a78",
"before": "117043493904907452086401",
"after": "1880006111700164990629283",
"delta": "1762962617795257538542882"
}
Origin: exploit balance diff.
These deltas match the root-cause claim precisely: the pair is drained of USDT, pair IPC inventory collapses, and the configured reward pool receives the newly minted IPC produced by _destroy().
The adversary cluster contains the profit-taking EOA 0x09ea8b5e546914746f3dc686ac164486a607fb7b and helper contract 0x3be77a356848cf7220503e62e93dfd0ff3f0074a. The EOA submits the exploit transaction; the helper executes all flashloan and swap logic on-chain.
Stage 1 is flashloan funding. The helper borrows 256285582578788161478508 raw USDT units from DODO pool 0x6098A5638d8D7e9Ed2f952d35B2b67c34EC6B476 and 77794276765052816860394 raw USDT units from DODO pool 0x0e15e47C3DE9CD92379703cf18251a2D13E155A7. Those are public liquidity sources, so the exploit needs no privileged capital.
Stage 2 is the manipulated loop. On each iteration, the helper calls the pair directly so the pair emits a transient 1 wei USDT outflow alongside IPC output. During the IPC transfer, _isRemoveLP(pair) sees balanceOf(USDT, pair) < reserve, so IPC treats the transfer as LP removal rather than a buy. The helper then sells the received IPC through PancakeRouter, which triggers _destroy(destroyNum) and updates destroyNum for the next round. This is why the attack becomes more profitable as the loop advances: every sell burns earlier inventory from the pair and locks in lower reserves via sync().
Stage 3 is cleanup and profit realization. After the sixteenth round, the helper repays both DODO lenders and transfers the remaining USDT to the attacker EOA. The collector balance diff shows the attacker EOA's USDT rose from 36050997246860072025628 to 627985201199913649967454, for a net gain of 591934203953053577941826.
The measurable economic loss is the pair's USDT drain of 591934203953053577941826 raw units. The same transaction also reduced pair IPC balance by 991564846557401299746225 raw units and minted 1762962617795257538542882 raw IPC units to reward pool 0x054525bF471dfBAD447e27B45c763ce6e2B05a78.
The directly affected public components are the AI IPC token contract and the IPC/USDT Pancake pair. The exploit is permissionless under the observed pre-state because trading was already open, ownership was already renounced, and the attacker only needed public flashloans, standard AMM calls, and an attacker-deployed helper contract.
0x5ef1edb9749af6cec511741225e6d47103e0b647d1e41e08649caaff66942a910xEAb0d46682Ac707A06aEFB0aC72a91a3Fd6Fe5d10xDe3595a72f35d587e96d5C7B6f3E6C02ed2900AB0x054525bF471dfBAD447e27B45c763ce6e2B05a78collector seed metadata
collector balance diff
collector trace
collected IPC Token.sol source