Calculated from recorded token losses using historical USD prices at the incident time.
0xb33057f57ce451aa8cbb65508d298fe3c627509cc64a394736dace2671b6dcfa0x1ac5fac863c0a026e029b173f2ae4d33938ab473BSC0x7265553986a81c838867aa6b3625aba97b961f00BSCOn BSC, transaction 0xb33057f57ce451aa8cbb65508d298fe3c627509cc64a394736dace2671b6dcfa drained the IntrospectionToken-USDT Pancake V2 pair at 0x7265553986a81c838867aa6b3625aba97b961f00. The transaction sender was 0xb495573cd2246e7cc7d6d2b37d779463295e5ab0, the transaction target and flash borrower was 0xa199a94379c9a518e3853516ac33fca122a7bb1d, and the in-transaction worker was 0x0f3e75e4552eabbe2777d78e940e9552eb9c8d8b.
The adversary borrowed 2000000000000000000000 USDT from DODO flash liquidity at 0x92b7807bf19b7dddf89b706143896d05228f3121, repeatedly transferred 2000000000000000000000 USDT to the IntrospectionToken-USDT pair, and called PancakePair::swap directly for both IntrospectionToken and USDT output. The pair lost 12414094106519345202759 USDT. The adversary then routed 12413094106519345202759 USDT through PancakeRouter into secondary pair 0xcabacfcca362e365132be54619278b129413cf7b, received 499959622262620716963 units of token 0x7c82a1a73b1da4a3daa398fc9e1a0053d2d5d695, and repaid the flash loan with fee.
The root cause is in IntrospectionToken 0x1ac5fac863c0a026e029b173f2ae4d33938ab473: when the Pancake pair is the sender in , the token mints new IntrospectionToken to that same pair before PancakePair finishes swap accounting. Pancake V2 computes input amounts from post-transfer balances, so this in-swap mint is credited as current swap input. An unprivileged caller can therefore withdraw USDT while supplying only USDT and relying on the token contract to fabricate the IntrospectionToken input.
_transferPancake V2 pairs maintain reserves and, during swap, verify that enough input arrived by comparing current token balances against reserves and requested output amounts. This model assumes balance increases counted as input came from the swap caller or from assets deliberately transferred before the swap, not from the output token minting new units to the pair during its own transfer.
The relevant pool is the IntrospectionToken-USDT Pancake pair:
0x1ac5fac863c0a026e029b173f2ae4d33938ab4730x55d398326f99059ff775485246999027b31979550x7265553986a81c838867aa6b3625aba97b961f000x10ed43c718714eb63d5aa57b78b54704e256024eThe pair's token ordering is IntrospectionToken as token0 and USDT as token1. Before the exploit transaction, the balance diff artifact records the pair holding 136086058894280573915886 IntrospectionToken units and 15363182364244763656536 USDT units. After the transaction, the pair held 876132225448633231977384 IntrospectionToken units and 2949088257725418453777 USDT units.
The vulnerability is an AMM integration break caused by IntrospectionToken's transfer-side minting logic. In BEP20._transfer, a transfer whose sender is the configured exchange enters a special branch. That branch calls mintToPoolIfNeeded(amount), which computes a mint amount from pair reserves and the pair's current USDT balance, then calls _mint(address(exchange), mintAmount).
During a Pancake pair swap, the pair transfers output token0 by calling IntrospectionToken::transfer. Because the pair is the token transfer sender, IntrospectionToken mints additional token0 into the pair during that output transfer. The pair then reads post-transfer token balances and computes amount0In; the newly minted IntrospectionToken appears as input, even though it came from the token contract itself. The adversary compounds this by supplying USDT before each swap and requesting both IntrospectionToken and USDT output.
The violated invariant is: a token output transfer from an AMM pair must not create additional balance in that same pair that the AMM credits as input for the same swap. The concrete code breakpoint is the sender == exchange branch in BEP20._transfer before the final pair balance checks in PancakePair::swap.
The collected verified IntrospectionToken source shows the vulnerable transfer branch:
function _transfer(address sender, address recipient, uint256 amount) internal {
...
if(address(exchange) == sender) {
if(exchange.totalSupply() >= _lastExchangeTotalSupply) {
updateCurrMaxUsdtRateIfNeeded();
_makeReferralUnlocksIfNeeded(recipient, amount);
mintToPoolIfNeeded(amount);
_mint(_owner, amount.div(20));
}
}
...
require(amount <= availableBalanceOf(sender), "BEP20: transfer amount exceeds available balance");
...
_balances[sender] = _balances[sender].sub(sendAmount);
_balances[recipient] = _balances[recipient].add(receiveAmount);
}
The mint helper directly mints to the configured exchange, which is the Pancake pair:
function mintToPoolIfNeeded (uint256 amount) internal {
...
(uint112 reserve0, uint112 reserve1, ) = exchange.getReserves();
...
uint256 usdtReserveAfterBuy =
this.min(tokenReserve.mul(usdtReserve).div(tokenReserveAfterBuy),
usdtToken.balanceOf(address(exchange)));
...
_mint(address(exchange), mintAmount);
}
The transaction trace confirms that this code path executes inside PancakePair::swap. In one representative swap, the worker first transfers 2000000000000000000000 USDT to the pair, then calls:
PancakePair::swap(
amount0Out = 277819756712274568828773,
amount1Out = 3383187818172762510087,
to = 0x0F3E75E4552EABbE2777d78E940E9552eB9C8D8b
)
IntrospectionToken::transfer(..., 277819756712274568828773)
emit Transfer(
from: 0x0000000000000000000000000000000000000000,
to: 0x7265553986a81c838867aA6B3625ABA97B961f00,
value: 342704623330057578784710
)
BEP20USDT::transfer(..., 3383187818172762510087)
emit Swap(
amount0In: 342704623330057578784710,
amount1In: 2000000000000000000000,
amount0Out: 277819756712274568828773,
amount1Out: 3383187818172762510087
)
The Transfer event from the zero address to the pair is the mint. The subsequent Swap event records the same minted quantity as amount0In, proving that the pair accepted the minted IntrospectionToken as input for that swap. This is the deterministic mechanism that lets the attacker withdraw USDT from the pair while repeatedly using the token's own minting behavior to satisfy Pancake's invariant check.
The end-to-end transaction sequence is a single BSC transaction at block 36934259.
0xb495573cd2246e7cc7d6d2b37d779463295e5ab0 sends the transaction to helper 0xa199a94379c9a518e3853516ac33fca122a7bb1d.2000000000000000000000 USDT from DODO flash lender 0x92b7807bf19b7dddf89b706143896d05228f3121.0x0f3e75e4552eabbe2777d78e940e9552eb9c8d8b and funds it with USDT.2000000000000000000000 USDT into 0x7265553986a81c838867aa6b3625aba97b961f00 and calls swap directly. Each IntrospectionToken output transfer triggers mintToPoolIfNeeded, causing IntrospectionToken mints to the pair that PancakePair records as input.12413094106519345202759 USDT into token 0x7c82a1a73b1da4a3daa398fc9e1a0053d2d5d695, receiving 499959622262620716963 units.2000200000000000000000 USDT to the flash lender. The balance diff records the lender's USDT delta as +200000000000000000, matching the flash fee.The flow is permissionless. It uses public flash liquidity, direct public pair calls, verified token behavior, and public router calls. The helper and worker are adversary-side execution artifacts, but they are not privileged protocol components.
The victim IntrospectionToken-USDT pair lost 12414094106519345202759 raw USDT units, with USDT using 18 decimals on BSC in this artifact set.
Balance diff evidence:
{
"token": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0x7265553986a81c838867aa6b3625aba97b961f00",
"before": "15363182364244763656536",
"after": "2949088257725418453777",
"delta": "-12414094106519345202759"
}
The same balance diff records the pair's IntrospectionToken balance increasing by 740046166554352658061498 units, consistent with repeated token mints into the pair during swaps. The adversary also moved 12413094106519345202759 USDT into secondary pair 0xcabacfcca362e365132be54619278b129413cf7b and received 499959622262620716963 units of 0x7c82a1a73b1da4a3daa398fc9e1a0053d2d5d695.
0xb33057f57ce451aa8cbb65508d298fe3c627509cc64a394736dace2671b6dcfa/workspace/session/artifacts/collector/seed/56/0xb33057f57ce451aa8cbb65508d298fe3c627509cc64a394736dace2671b6dcfa/metadata.json/workspace/session/artifacts/collector/seed/56/0xb33057f57ce451aa8cbb65508d298fe3c627509cc64a394736dace2671b6dcfa/trace.cast.log/workspace/session/artifacts/collector/seed/56/0xb33057f57ce451aa8cbb65508d298fe3c627509cc64a394736dace2671b6dcfa/balance_diff.json/workspace/session/artifacts/collector/seed/56/0x1ac5fac863c0a026e029b173f2ae4d33938ab473/src/bsc/BEP20.sol0x7265553986a81c838867aa6b3625aba97b961f000x92b7807bf19b7dddf89b706143896d05228f3121