This is a lower bound: only assets with reliable historical USD prices are counted, so the actual loss may be higher.
0x75a26224da9faf37c2b3a4a634a096af7fec561f631a02c93e11e4a19d1594770x13b1f2e227ca6f8e08ac80368fd637f5084f10a5BSC0xa4227de36398851aebf4a2506008d0aab2dd0e71BSC0xa19d2674a8e2709a92e04403f721d8448f802e1fBSCIn BSC transaction 0x75a26224da9faf37c2b3a4a634a096af7fec561f631a02c93e11e4a19d159477 at block 33528883, an unprivileged adversary used a public DODO flash loan to buy SHIBA from the public sale contract at 0xa4227de36398851aebf4a2506008d0aab2dd0e71, then immediately sold that supposedly locked inventory into the Pancake SHIBA/USDT pair at 0xa19d2674a8e2709a92e04403f721d8448f802e1f. The transaction left the adversary cluster with a deterministic net gain of 101.695682799492366219 WBNB-equivalent after gas.
The root cause is a token-locking failure in SHIBA at 0x13b1f2e227ca6f8e08ac80368fd637f5084f10a5. The token enforces getAvailableBalance(...) on transfer and transferFrom, but its public helper functions transferLockToken and batchTransferLockToken call super.transfer(...) directly without any sender-side spendability check. That mismatch lets locked balances move anyway, so the sale contract's "locked presale inventory" guarantee does not hold.
The public sale contract distributes SHIBA through transferLockToken, not through ordinary transfer. Buyers are therefore supposed to receive SHIBA that remains non-transferable until the token's unlock schedule reduces .
users[wallet].lockedBalanceThe critical design detail is that SHIBA exposes three outbound paths with different enforcement:
transfer and transferFrom require getAvailableBalance(sender) >= amount.transferLockToken and batchTransferLockToken increase the recipient's locked accounting and then execute super.transfer(...).The exploit only needed public infrastructure: DODO flash liquidity from 0xfeafe253802b77456b4627f8c2306a9cebb5d681, the public sale, Pancake liquidity, and the public router at 0x10ed43c718714eb63d5aa57b78b54704e256024e.
This incident is an ACT attack caused by inconsistent enforcement of a lock invariant. The intended invariant is straightforward: if users[x].lockedBalance is positive, that locked portion must not leave x until unlock() reduces it. SHIBA enforces that rule only on the standard ERC-20 transfer paths, not on the public lock-helper paths. As a result, the sale contract can successfully deliver "locked" SHIBA to a buyer, while the buyer can immediately move the same balance through batchTransferLockToken into an AMM pair. The pair accepts the balance as ordinary inventory and pays out USDT, even though the token still records the sender as locked. The exploit is therefore not a pricing bug, oracle bug, or access-control bug in the sale or DEX; it is a code-level lock-bypass in the token contract that turns discounted presale inventory into instantly dumpable inventory.
The core code-path mismatch is visible in the verified SHIBA token source:
function transferLockToken(address _wallet, uint256 _amount) public {
users[_wallet].lockedBalance = users[_wallet].lockedBalance.add(_amount);
users[_wallet].unlockPerSecond = users[_wallet].lockedBalance.mul(unlockPercent).div(100).div(duration);
super.transfer(_wallet, _amount);
}
function batchTransferLockToken(Airdrop[] memory _airdrops) public {
for (uint256 i = 0; i < _airdrops.length; i++) {
address wallet = _airdrops[i].wallet;
uint256 amount = _airdrops[i].amount;
users[wallet].lockedBalance = users[wallet].lockedBalance.add(amount);
users[wallet].unlockPerSecond = users[wallet].lockedBalance.mul(unlockPercent).div(100).div(duration);
super.transfer(wallet, amount);
}
}
function transfer(address _to, uint256 _amount) public override returns (bool) {
uint256 availableAmount = getAvailableBalance(_msgSender());
require(availableAmount >= _amount, "Not Enough Available Token");
return super.transfer(_to, _amount);
}
That is the breakpoint. The lock invariant is checked in transfer and transferFrom, but bypassed in the public helper functions that the sale itself depends on.
The incident trace shows the full exploit chain:
flashLoan(20000000000000000000, 0, attacker_contract, abi.encode(1))
buyByBnb(address(0))
transferLockToken(attacker_contract, 507677278570125202361500000)
batchTransferLockToken([{wallet: 0xa19d2674a8e2709a92e04403f721d8448f802e1f, amount: 507677278570125202361500000}])
PancakePair.swap(0, 30948073916467640719090, attacker_contract, bytes(0))
swapExactTokensForETHSupportingFeeOnTransferTokens(30948073916467640719090, 0, [USDT, WBNB], attacker_contract, 1700095314)
The balance diff confirms the economic effect:
{
"sale_shiba_delta": "-507677278570125202361500000",
"pair_shiba_delta": "507677278570125202361500000",
"pair_usdt_delta": "-30948073916467640719090",
"gas_cost_wei": "1541919000000000",
"cluster_net_profit_wei": "101695682799492366219"
}
Post-state checks make the broken invariant explicit. After the dump, the attacker's SHIBA balance is zero, but getLockedBalance(attacker) is still 507677278570125202361500000. In the captured post-state, getAvailableBalance(attacker) would underflow because the token still considers the attacker locked after the tokens have already left through batchTransferLockToken.
The adversary lifecycle is deterministic and entirely on-chain:
0xb9bdc2537c6f4b587a5c81a67e7e3a4e6ddda189 calls helper contract 0xda148143379ae54e06d2429a5c80b19d4a9d6734.20 WBNB from the DODO pool and unwraps it into native BNB.buyByBnb(address(0)) on the SHIBA sale contract with the full 20 BNB.507677278570125202361500000 SHIBA to the attacker through transferLockToken, marking the balance as locked.batchTransferLockToken and re-lock-transfers the full balance into the Pancake SHIBA/USDT pair.30948073916467640719090 USDT, which the attacker routes through Pancake into WBNB.20 WBNB flash loan and sends 101697224718492366219 WBNB to profit-recipient 0x1874726c8c9a501836929f495a8b44968fbfdad8.Nothing in that sequence requires a privileged key, private orderflow, or attacker-specific off-chain artifact. The exploit predicate is simply the existence of purchasable locked SHIBA plus the public helper functions that allow those locked tokens to move.
The measurable losses are:
507677278570125202361500000 SHIBA (decimal=18) that it intended to distribute as locked inventory.30948073916467640719090 USDT (decimal=18) when it bought SHIBA delivered through the bypass path.101695682799492366219 wei-equivalent of net profit after gas.Affected components are the SHIBA token contract, the SHIBA sale contract, and the SHIBA/USDT Pancake pool. The token contract is the root-cause component; the sale and pair are the economically impacted components.
0x75a26224da9faf37c2b3a4a634a096af7fec561f631a02c93e11e4a19d1594770x13b1f2e227ca6f8e08ac80368fd637f5084f10a50xa4227de36398851aebf4a2506008d0aab2dd0e710xfeafe253802b77456b4627f8c2306a9cebb5d6810x10ed43c718714eb63d5aa57b78b54704e256024e0xa19d2674a8e2709a92e04403f721d8448f802e1f/workspace/session/artifacts/collector/seed/56/0x13b1f2e227ca6f8e08ac80368fd637f5084f10a5/src/Token.sol/workspace/session/artifacts/collector/seed/56/0x75a26224da9faf37c2b3a4a634a096af7fec561f631a02c93e11e4a19d159477/trace.cast.log and /workspace/session/artifacts/auditor/iter_0/key_evidence.json/workspace/session/artifacts/collector/seed/56/0x75a26224da9faf37c2b3a4a634a096af7fec561f631a02c93e11e4a19d159477/balance_diff.json