SHIBA Lock Bypass
Exploit Transactions
0x75a26224da9faf37c2b3a4a634a096af7fec561f631a02c93e11e4a19d159477Victim Addresses
0x13b1f2e227ca6f8e08ac80368fd637f5084f10a5BSC0xa4227de36398851aebf4a2506008d0aab2dd0e71BSC0xa19d2674a8e2709a92e04403f721d8448f802e1fBSCLoss Breakdown
Similar Incidents
Bitpaid Mature-Lock Top-Up Exploit
35%BSC PoolWithdraw Signature-Bypass Drains USDT Pool
33%BTNFT transferFrom reward-claim bypass drains vested BTTToken rewards
31%NeverFallToken LP Drain
31%Sheep Burn Reserve Drain
30%ARA Swap Helper Approved-Holder Exploit
30%Root Cause Analysis
SHIBA Lock Bypass
1. Incident Overview TL;DR
In 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.
2. Key Background
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].lockedBalance.
The critical design detail is that SHIBA exposes three outbound paths with different enforcement:
transferandtransferFromrequiregetAvailableBalance(sender) >= amount.transferLockTokenandbatchTransferLockTokenincrease the recipient's locked accounting and then executesuper.transfer(...).- Because those helper paths are public, any holder of locked SHIBA can call them directly.
The exploit only needed public infrastructure: DODO flash liquidity from 0xfeafe253802b77456b4627f8c2306a9cebb5d681, the public sale, Pancake liquidity, and the public router at 0x10ed43c718714eb63d5aa57b78b54704e256024e.
3. Vulnerability Analysis & Root Cause Summary
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.
4. Detailed Root Cause Analysis
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.
5. Adversary Flow Analysis
The adversary lifecycle is deterministic and entirely on-chain:
- The gas-paying EOA
0xb9bdc2537c6f4b587a5c81a67e7e3a4e6ddda189calls helper contract0xda148143379ae54e06d2429a5c80b19d4a9d6734. - The helper contract flash-borrows
20WBNB from the DODO pool and unwraps it into native BNB. - It calls
buyByBnb(address(0))on the SHIBA sale contract with the full20BNB. - The sale contract transfers
507677278570125202361500000SHIBA to the attacker throughtransferLockToken, marking the balance as locked. - The attacker immediately calls
batchTransferLockTokenand re-lock-transfers the full balance into the Pancake SHIBA/USDT pair. - The pair pays out
30948073916467640719090USDT, which the attacker routes through Pancake into WBNB. - The attacker repays the
20WBNB flash loan and sends101697224718492366219WBNB to profit-recipient0x1874726c8c9a501836929f495a8b44968fbfdad8.
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.
6. Impact & Losses
The measurable losses are:
- The sale contract lost
507677278570125202361500000SHIBA (decimal=18) that it intended to distribute as locked inventory. - The Pancake SHIBA/USDT pair lost
30948073916467640719090USDT (decimal=18) when it bought SHIBA delivered through the bypass path. - The adversary cluster realized
101695682799492366219wei-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.
7. References
- Incident transaction:
0x75a26224da9faf37c2b3a4a634a096af7fec561f631a02c93e11e4a19d159477 - SHIBA token:
0x13b1f2e227ca6f8e08ac80368fd637f5084f10a5 - SHIBA sale contract:
0xa4227de36398851aebf4a2506008d0aab2dd0e71 - DODO flash-loan pool:
0xfeafe253802b77456b4627f8c2306a9cebb5d681 - Pancake router:
0x10ed43c718714eb63d5aa57b78b54704e256024e - SHIBA/USDT pair:
0xa19d2674a8e2709a92e04403f721d8448f802e1f - Token source:
/workspace/session/artifacts/collector/seed/56/0x13b1f2e227ca6f8e08ac80368fd637f5084f10a5/src/Token.sol - Trace and decoded evidence:
/workspace/session/artifacts/collector/seed/56/0x75a26224da9faf37c2b3a4a634a096af7fec561f631a02c93e11e4a19d159477/trace.cast.logand/workspace/session/artifacts/auditor/iter_0/key_evidence.json - Balance deltas:
/workspace/session/artifacts/collector/seed/56/0x75a26224da9faf37c2b3a4a634a096af7fec561f631a02c93e11e4a19d159477/balance_diff.json