This is a lower bound: only assets with reliable historical USD prices are counted, so the actual loss may be higher.
0x96a955304fed48a8fbfb1396ec7658e7dc42b7c140298b80ce4206df34f40e8d0xabc6e5a63689b8542dbdc4b4f39a7e00d4ac30c8BSC0x35886c6d74aced4ed0fbe0b851806278384d9a76BSCOn BSC (chainid 56), a single contract-creation transaction
0x96a955304fed48a8fbfb1396ec7658e7dc42b7c140298b80ce4206df34f40e8d
from unprivileged EOA 0x56b2d55457b31fb4b78ebddd6718ea2667804a06
deploys an adversary-controlled helper/orchestrator contract. During its
constructor execution, this helper abuses a broken
DexToken::transferFrom implementation to move essentially the entire
DexToken balance held by the DexToken contract itself
0xabc6e5a63689b8542dbdc4b4f39a7e00d4ac30c8 into an adversary swap
executor contract 0x0496824589cd3758119f74560e4fa970e6dff104 and into
the DexToken‑USDT Pancake pair
0x35886c6d74aced4ed0fbe0b851806278384d9a76.
The swap executor then repeatedly sells the stolen DexToken into the
DexToken‑USDT pair via PancakeRouter, draining exactly
13,038.598589899306674112 BEP20USDT from the pool. By the end of the
transaction, the attacker EOA holds 7,251.288075182757035057 USDT and
has paid 0.017506866 BNB in gas, while two additional addresses receive
the remaining drained USDT.
The root cause is a logic bug in DexToken’s transferFrom function
(implemented under Solidity ^0.7.6). The function calls
_transfer(sender, recipient, amount) without enforcing
allowance[sender][msg.sender] >= amount and then applies an unchecked
subtraction to _allowances[sender][msg.sender]. Under Solidity 0.7.6
(this code path does not use SafeMath), subtraction underflows silently
instead of reverting. As a result, any caller can transfer tokens from
any address, including the DexToken contract itself, as long as the
source address has sufficient balance.
DexToken is an Ownable ERC20-style token deployed on BSC at
0xabc6e5a63689b8542dbdc4b4f39a7e00d4ac30c8. The verified source
(LW.sol) shows that it implements custom fee-on-transfer behavior, an
internal AMM helper _internalSwap, and an lpBurn(uint256) function
that interacts with a Pancake-style liquidity pool.
// DexToken (excerpt)
contract DexToken is LinkingTheWorld {
// ...
// 转账
function transfer(
address recipient,
uint256 amount
) public override returns (bool) {
_transfer(msg.sender, recipient, amount);
return true;
}
// 转账
function transferFrom(
address sender,
address recipient,
uint256 amount
) public override returns (bool) {
_transfer(sender, recipient, amount);
if (_allowances[sender][msg.sender] != MAX) {
_allowances[sender][msg.sender] =
_allowances[sender][msg.sender] -
amount;
}
return true;
}
// 内部转账方法
function _transfer(address from, address to, uint256 amount) private {
// 黑名单无法买卖和转账
if (_blackList[from] || _blackList[to]) {
require(false);
}
// 判断余额
uint256 balance = _balances[from];
require(balance >= amount, "balanceNotEnough");
// ... fee and swap logic elided ...
_tokenTransfer(from, to, amount, takeFee, isSell, isTransfer);
}
}
Snippet origin: DexToken verified source LW.sol for
0xabc6e5a63689b8542dbdc4b4f39a7e00d4ac30c8.
The DexToken contract stores balances in an internal _balances mapping
and allowances in _allowances. The constant MAX is used to represent
an “infinite” approval, but there is no explicit check that the current
allowance is at least the requested transfer amount before subtraction.
Because the contract is compiled with pragma solidity ^0.7.6 and does
not wrap the allowance subtraction in SafeMath, a subtraction that would
underflow simply wraps modulo 2^256 rather than reverting.
The DexToken‑USDT pool is a standard Pancake (Uniswap V2–style) pair
contract at 0x35886c6d74aced4ed0fbe0b851806278384d9a76. Its verified
source enforces the usual x*y = k invariant and a 0.25% fee.
interface IPancakePair {
event Swap(
address indexed sender,
uint amount0In,
uint amount1In,
uint amount0Out,
uint amount1Out,
address indexed to
);
event Sync(uint112 reserve0, uint112 reserve1);
function getReserves() external view returns (
uint112 reserve0,
uint112 reserve1,
uint32 blockTimestampLast
);
function swap(
uint amount0Out,
uint amount1Out,
address to,
bytes calldata data
) external;
}
Snippet origin: PancakePair flattened source for
0x35886c6d74aced4ed0fbe0b851806278384d9a76.
BEP20USDT is deployed at 0x55d398326f99059ff775485246999027b3197955.
The local verified source confirms it is a standard BEP20 implementation
with 18 decimals and standard allowance semantics.
contract BEP20USDT is Context, IBEP20, Ownable {
using SafeMath for uint256;
mapping (address => uint256) private _balances;
mapping (address => mapping (address => uint256)) private _allowances;
uint256 private _totalSupply;
uint8 public _decimals;
string public _symbol;
string public _name;
constructor() public {
_name = "Tether USD";
_symbol = "USDT";
_decimals = 18;
_totalSupply = 30000000000000000000000000;
_balances[msg.sender] = _totalSupply;
emit Transfer(address(0), msg.sender, _totalSupply);
}
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) {
_transfer(sender, recipient, amount);
_approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(
amount, "BEP20: transfer amount exceeds allowance"));
return true;
}
}
Snippet origin: BEP20USDT verified source for
0x55d398326f99059ff775485246999027b3197955.
The adversary-related cluster comprises:
0x56b2d55457b31fb4b78ebddd6718ea2667804a06 — the sender of the
seed contract-creation transaction, which pays gas and ends the
transaction holding 7,251.288075182757035057 USDT.0xfe7E9C76affDBa7b7442adACa9C7c059ec3092FC — deployed by the seed
transaction (metadata shows to=null and non-empty runtime bytecode
at this address), which orchestrates calls into DexToken and the swap
executor.0x0496824589cd3758119f74560e4fa970e6dff104
— receives the stolen DexToken and acts as the caller to PancakeRouter
in the exploit trace. An eth_getCode query returns non-empty
bytecode embedding the DexToken, BEP20USDT, and PancakeRouter
addresses.Normal transaction histories for DexToken, the DexToken‑USDT pool, the
attacker EOA, the swap executor, and the two large USDT recipients were
fetched and reviewed via Etherscan API. These txlists confirm there are
no prior approval transactions from the DexToken contract to either the
helper or the swap executor, consistent with the claim that the exploit
relies solely on the broken transferFrom logic.
The core vulnerability is an incorrect implementation of
DexToken::transferFrom under Solidity ^0.7.6. Instead of enforcing the
standard ERC20 invariant that transferFrom succeeds only when
allowance[sender][caller] >= amount (unless an infinite approval is in
place), DexToken executes the internal transfer first and only then
applies an unchecked subtraction to the allowance. Because arithmetic in
Solidity 0.7.6 does not revert on underflow when used directly on
uint256, an attacker can call transferFrom with zero approval and
have the allowance underflow without error.
The relevant invariant can be stated as:
For all addresses s (source), c (caller), r (recipient) and all amounts a, if
transferFrom(s, r, a)executed by caller c returns success, then (1)a <= allowance[s][c]unlessallowance[s][c]equalsMAX(infinite approval), and (2)a <= balanceOf[s].
DexToken breaks this invariant at the following breakpoint:
function transferFrom(
address sender,
address recipient,
uint256 amount
) public override returns (bool) {
_transfer(sender, recipient, amount);
if (_allowances[sender][msg.sender] != MAX) {
_allowances[sender][msg.sender] =
_allowances[sender][msg.sender] -
amount;
}
return true;
}
There is no require enforcing allowance[sender][msg.sender] >= amount
before subtraction. Underflow wraps silently, and the actual transfer
succeeds as long as _balances[sender] >= amount at the time of
_transfer.
In the pre-state immediately before the exploit transaction (block 40,287,545), the on-chain artifacts show:
0xabc6e5a63689b8542dbdc4b4f39a7e00d4ac30c8 is
1,000,015,840,000,000,000,000,000,000,000,000 DexToken units.0x3588...9a76 holds
793,993,204,459,944,103,014,021,705 DexToken and
13,256,433,814,164,156,746,766 BEP20USDT.0x56b2...4a06 holds 0 BEP20USDT and
0.997500000000000001 BNB.These balances are taken directly from the seed transaction metadata and balance diff artifacts.
{
"erc20_balance_deltas": [
{
"token": "0xabc6e5a63689b8542dbdc4b4f39a7e00d4ac30c8",
"holder": "0xabc6e5a63689b8542dbdc4b4f39a7e00d4ac30c8",
"before": "115792089237316195423570985008687907853269984665640563583345235133956513969238",
"after": "115792089237316195423570985008687907853269983665624723583345235133956513969238",
"delta": "-1000015840000000000000000000000000"
},
{
"token": "0xabc6e5a63689b8542dbdc4b4f39a7e00d4ac30c8",
"holder": "0x0496824589cd3758119f74560e4fa970e6dff104",
"before": "0",
"after": "999952000000000000000000000000000",
"delta": "999952000000000000000000000000000"
},
{
"token": "0xabc6e5a63689b8542dbdc4b4f39a7e00d4ac30c8",
"holder": "0x35886c6d74aced4ed0fbe0b851806278384d9a76",
"before": "793993204459944103014021705",
"after": "48793993204459944103014021705",
"delta": "48000000000000000000000000000"
}
]
}
Snippet origin: seed transaction balance_diff.json for
0x96a9...40e8d on BSC.
The first line shows the DexToken contract’s own balance decreasing by
1,000,015,840,000,000,000,000,000,000,000,000 units. The next two
lines show that this exact amount is split into
999,952,000,000,000,000,000,000,000,000,000 DexToken transferred to the
swap executor 0x0496... and 48,000,000,000,000,000,000,000,000,000
DexToken transferred to the DexToken‑USDT pair.
The seed transaction trace corroborates that this movement is driven by a
call to DexToken::transferFrom with sender set to the DexToken
contract itself and recipient set to 0x0496...:
│ ├─ [65526] DexToken::transferFrom(DexToken: [0xABC6e5a63689b8542dbDC4b4f39a7e00d4AC30c8], 0x0496824589CD3758119F74560E4Fa970e6dff104, 1000000000000000000000000000000000 [1e33])
│ │ ├─ [151952] DexToken::transferFrom(0x0496824589CD3758119F74560E4Fa970e6dff104, PancakePair: [0x35886C6D74ACed4Ed0fbe0b851806278384D9A76], 800000000000000000000000000 [8e26])
│ │ ├─ [58568] DexToken::transferFrom(0x0496824589CD3758119F74560E4Fa970e6dff104, PancakePair: [0x35886C6D74ACed4Ed0fbe0b851806278384D9A76], 800000000000000000000000000 [8e26])
... (multiple repeated DexToken::transferFrom calls from 0x0496... to the pair)
Snippet origin: seed transaction trace.cast.log for
0x96a9...40e8d on BSC.
Because there is no approval from the DexToken contract to the helper or
swap executor in DexToken’s tx history, and because the DexToken
contract is not an EOA capable of signing approvals, the only viable
explanation for this movement is the broken transferFrom allowance
semantics. The adversary calls transferFrom targeting the token
contract’s own balance, and the function performs the transfer without
checking allowance, then underflows _allowances without reverting.
This section describes step-by-step how the vulnerability is turned into
an exploit in the concrete transaction
0x96a955304fed48a8fbfb1396ec7658e7dc42b7c140298b80ce4206df34f40e8d on
BSC.
Immediately before the seed transaction is included in block 40,287,545,
balance_diff.json and the associated metadata establish the following
pre-state σ_B:
0xabc6...30c8) holds
1,000,015,840,000,000,000,000,000,000,000,000 DexToken.0x3588...9a76) holds
793,993,204,459,944,103,014,021,705 DexToken and
13,256,433,814,164,156,746,766 BEP20USDT.0x56b2...4a06) has 0 BEP20USDT and
0.997500000000000001 BNB.These conditions are fully determined from on-chain data (RPC metadata and balance diffs). Any unprivileged adversary observing the chain can verify that such a pre-state exists.
The seed transaction is a legacy (type-0) contract-creation transaction
with to=null, value=0, and gas parameters consistent with ordinary
BSC usage. Metadata for the transaction shows:
{
"result": {
"from": "0x56b2d55457b31fb4b78ebddd6718ea2667804a06",
"to": null,
"gas": "0x989680",
"gasPrice": "0xb2d05e00",
"value": "0x0",
"type": "0x0",
"chainId": "0x38"
}
}
Snippet origin: seed transaction metadata.json for
0x96a9...40e8d on BSC.
An eth_getCode call for the resulting contract address
0xfe7E9C76affDBa7b7442adACa9C7c059ec3092FC shows non-empty runtime
bytecode. Combined with the fact that this address is not present as a
destination in any prior transaction, we can deterministically identify
it as the helper/orchestrator deployed by the seed transaction.
eth_getCode for the sender 0x56b2...4a06 returns 0x, confirming it
is an EOA.
Within the reconstructed trace, the depth-1 frame corresponds to the
constructor of 0xfe7E9C76.... That constructor immediately executes a
sequence of calls into DexToken and the swap executor, which together
realize the exploit.
The first critical operation is the theft of the DexToken contract’s own balance. The trace clearly shows a call to
DexToken::transferFrom(
DexToken: [0xABC6e5a63689b8542dbDC4b4f39a7e00d4AC30c8],
0x0496824589CD3758119F74560E4Fa970e6dff104,
1000000000000000000000000000000000 [1e33]
)
This call uses sender = 0xabc6...30c8 (the DexToken contract itself),
recipient = 0x0496... (the swap executor), and
amount = 1e33 = 1,000,000,000,000,000,000,000,000,000,000,000 DexToken.
Given the exact pre-state balance of the DexToken contract, this amount
corresponds to “essentially all” of the DexToken balance held at the
contract address (the slight difference between 1e33 and
1,000,015,840,000,000,000,000,000,000,000,000 is accounted for by
additional movements into the DexToken‑USDT pair visible in
balance_diff.json).
Because transferFrom in DexToken:
_transfer, and_allowances[sender][msg.sender]
under Solidity 0.7.6,any caller can execute this function as long as _balances[sender]
contains enough tokens. The DexToken contract address holds a large
balance in the pre-state, so the helper/orchestrator’s call to
transferFrom succeeds with no prior approval.
balance_diff.json confirms the net effect:
1,000,015,840,000,000,000,000,000,000,000,000.0x0496... receives
999,952,000,000,000,000,000,000,000,000,000 DexToken.48,000,000,000,000,000,000,000,000,000 DexToken.There is no approval from the DexToken contract to either the helper or
swap executor in the DexToken txlist. Under standard ERC20 semantics,
any such approval would need to be a transaction from the DexToken
contract’s address, which cannot exist because the DexToken contract is
not an EOA. Therefore, the only mechanism that can explain these
movements is the broken transferFrom implementation.
Once the swap executor 0x0496... holds nearly all stolen DexToken, it
repeatedly sells DexToken into the DexToken‑USDT pair via
PancakeRouter::swapExactTokensForTokensSupportingFeeOnTransferTokens.
The trace shows multiple nested calls where 0x0496... is the caller to
PancakeRouter, leading to DexToken::transferFrom calls from
0x0496... to the pair and then to PancakePair::swap calls that
produce USDT out of the pool.
balance_diff.json for BEP20USDT shows the resulting deltas:
{
"erc20_balance_deltas": [
{
"token": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0x35886c6d74aced4ed0fbe0b851806278384d9a76",
"before": "13256433814164156746766",
"after": "217835224264850072654",
"delta": "-13038598589899306674112"
},
{
"token": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0x5adcefed6f5cfb2aafccf08ca3bfb388e08dd3ee",
"delta": "3446357976293290354724"
},
{
"token": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0x42d9c8e28db07f94d3aa36b41ab6f37ded8e2caa",
"delta": "2340952538423259284331"
},
{
"token": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0x56b2d55457b31fb4b78ebddd6718ea2667804a06",
"before": "0",
"after": "7251288075182757035057",
"delta": "7251288075182757035057"
}
]
}
Snippet origin: BEP20USDT section of seed transaction balance_diff.json
for 0x96a9...40e8d on BSC.
The pool loses exactly 13,038.598589899306674112 USDT
(-13038598589899306674112 in wei). The three recipient deltas sum
exactly to this loss:
3,446.357976293290354724 USDT to 0x5adce...3ee.2,340.952538423259284331 USDT to 0x42d9...2caa.7,251.288075182757035057 USDT to attacker EOA 0x56b2...4a06.This confirms that all USDT drained from the pool is distributed among these three addresses.
The adversary-related cluster’s profit is computed using BEP20USDT as
the reference asset. Focusing on the minimal adversary cluster
{EOA 0x56b2...4a06, helper 0xfe7E..., swap executor 0x0496...},
balance_diff.json shows:
+7,251.288075182757035057 USDT.0.017506866 BNB (from the native_balance_deltas section).Because the adversary spends 0 USDT in the transaction and ends with a positive USDT balance, the net change in the adversary’s portfolio value in units of BEP20USDT is strictly positive for any non-negative valuation of BNB in USDT. The ACT profit predicate holds deterministically:
value_before_in_reference_asset = 0 USDT.value_after_in_reference_asset = 7,251.288075182757035057 USDT.value_delta_in_reference_asset = 7,251.288075182757035057 USDT.fees_paid_in_reference_asset = 0 USDT (gas is paid in BNB only).The entire opportunity is realized within this single transaction.
This section describes the adversary’s end-to-end execution flow in
transaction 0x96a9...40e8d on BSC.
EOA submits contract-creation tx
0x56b2...4a06 submits a type-0 transaction with to=null and
input containing the creation bytecode for the helper
0xfe7E....Helper/orchestrator constructor executes
0xfe7E... begins
executing.0xabc6...30c8 invoking
DexToken::transferFrom with sender = 0xabc6...30c8,
recipient = 0x0496..., and amount = 1e33 DexToken.transferFrom does not
enforce an allowance check before transferring.DexToken contract balance drained to adversary
_transfer inside DexToken subtracts the amount from
_balances[0xabc6...30c8] and credits it to _balances[0x0496...],
while also sending a portion to the DexToken‑USDT pair._allowances[0xabc6...30c8][helper] is then decreased with an
unchecked subtraction, potentially underflowing, but the transaction
does not revert.Swap executor sells DexToken into USDT
0x0496... invokes
PancakeRouter::swapExactTokensForTokensSupportingFeeOnTransferTokens
multiple times, each time sending DexToken to the DexToken‑USDT pair
and receiving USDT out.swap and Sync events in the trace show reserves
updating as DexToken flows in and USDT flows out, consistent with
AMM pricing.USDT distribution to attacker and two additional addresses
0x56b2...4a06.0x5adce...3ee.0x42d9...2caa.balance_diff.json and confirmed by the ERC20 Transfer events in
the trace.Transaction final state
This flow uses only publicly observable contract code (DexToken,
PancakePair, BEP20USDT), standard AMM behavior, and the broken
transferFrom logic. Any unprivileged EOA could deploy a similar helper
contract and realize the same opportunity given the pre-state σ_B.
The measurable on-chain impact is fully determined from
balance_diff.json:
0x35886c6d74aced4ed0fbe0b851806278384d9a76
loses exactly 13,038.598589899306674112 BEP20USDT.1,000,015,840,000,000,000,000,000,000,000,000 DexToken, of which
999,952,000,000,000,000,000,000,000,000,000 are routed to the swap
executor and 48,000,000,000,000,000,000,000,000,000 are injected into
the DexToken‑USDT pair.0x56b2...4a06 realizes a net gain of
7,251.288075182757035057 BEP20USDT in a single transaction while
paying only 0.017506866 BNB in gas.0x5adcefed6f5cfb2aafccf08ca3bfb388e08dd3ee
and 0x42d9c8e28db07f94d3aa36b41ab6f37ded8e2caa, receive the remaining
drained USDT, but they are not included in the minimal adversary
cluster because available on-chain data does not deterministically
prove they are controlled by the same adversary.Liquidity providers in the DexToken‑USDT pool absorb the entire USDT
loss. The protocol’s token design (holding a large balance in the token
contract itself) amplifies the effect of the broken transferFrom by
making a large quantity of DexToken directly stealable without prior
user consent.
Key artifacts used to validate this root cause analysis:
0x96a955304fed48a8fbfb1396ec7658e7dc42b7c140298b80ce4206df34f40e8d
on BSC — contains raw RPC data (from, to, gas, gasPrice, input,
blockNumber, etc.) used to identify the transaction as a
contract-creation tx from 0x56b2...4a06 and link it to helper
contract 0xfe7E....balance_diff.json) — records
precise pre- and post-state balances for BNB, DexToken, and
BEP20USDT. Used to quantify the DexToken contract balance theft, the
USDT drained from the pool, and the final USDT holdings of the
attacker and other recipients.transferFrom, _transfer, _tokenTransfer, and
related functions, demonstrating the missing allowance check and
unchecked subtraction under Solidity 0.7.6.swap/Sync semantics consistent
with the observed USDT outflows.trace.cast.log) — provides an
annotated call tree showing helper constructor execution,
DexToken::transferFrom calls from the DexToken contract to
0x0496..., repeated DexToken::transferFrom calls from 0x0496...
to the pair, and the downstream AMM swaps that produce USDT.These artifacts are sufficient for an independent investigator to
reproduce the exploit flow, verify the root cause in DexToken’s
transferFrom, and confirm the profit and loss figures stated above.