Calculated from recorded token losses using historical USD prices at the incident time.
0x7e02ee7242a672fb84458d12198fae4122d7029ba64f3673e7800d811a8de93f0xfd80a436da2f4f4c42a5dbfa397064cfeb7d9508BSC0x927d7adf1bcee0fa1da868d2d43417ca7c6577d4BSCAn adversary-controlled helper contract exploited SATX on BSC in transaction 0x7e02ee7242a672fb84458d12198fae4122d7029ba64f3673e7800d811a8de93f at block 37914434. The helper used public flash liquidity, induced SATX/WBNB reserve corruption inside SATX transfer hooks, recovered surplus SATX with skim(), and exited into WBNB/BNB. The observed cluster-level profit was 49.336934003329246313 BNB after subtracting the funder's 0.900000001 BNB seed and 0.002628078 BNB gas.
The root cause is that SATX violates AMM accounting assumptions. When a transfer goes to the Pancake pair, SATX burns 2% of the pair's SATX inventory and immediately calls sync() before the surrounding swap flow finishes. That lets the attacker force the pair's stored reserves to reflect a token-manipulated intermediate state, after which skim() transfers the resulting surplus back to the attacker.
Pancake V2 pairs are safe only when token balances change through the pair's own accounting model: swaps, mint/burn, direct token transfers, and the pair's reserve update logic. skim() is intentionally simple: it transfers the current token balance minus the stored reserve. sync() does the opposite: it overwrites stored reserves with the current token balances.
That design becomes dangerous when a token contract mutates pair balances from inside transfer hooks. In SATX, pair-facing transfers are not passive ERC-20 movements. They can burn inventory from the pair, route taxes elsewhere, and force while a swap is still in flight. Once the pair's live balance and stored reserve diverge in the attacker's favor, becomes a direct extraction primitive.
sync()skim()SATX embeds pair-destructive logic directly inside its token transfer path. The critical function is destroyPoolToken(), which reads the pair balance, burns 2% of that balance from the pair to the dead address, and then calls sync() on the pair. _transfer() invokes that function whenever to == uniswapV2Pair, so ordinary sells into the pair trigger reserve mutation from token code rather than from pair code.
The relevant SATX logic is:
function destroyPoolToken() private {
uint256 haveAmount = super.balanceOf(uniswapV2Pair);
super._transfer(uniswapV2Pair, _deadAddress, haveAmount.div(100).mul(2));
IUniswapV2Pair(uniswapV2Pair).sync();
}
...
} else if(_isPairs[to]) {
destroyPoolToken();
super._transfer(from, address(this), amount.div(10000).mul(299));
amount = amount.div(10000).mul(9701);
}
PancakePair then exposes the exact extraction tools the attacker needs:
function skim(address to) external lock {
_safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
_safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
}
function sync() external lock {
_update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
}
The broken invariant is: the SATX/WBNB pair's reserves must remain a faithful accounting of the pair's actual balances after each user-observable step. SATX breaks that invariant by burning pair inventory and forcing reserve updates from inside the token contract. The first code-level breakpoint is the super._transfer(uniswapV2Pair, _deadAddress, haveAmount.div(100).mul(2)); plus IUniswapV2Pair(uniswapV2Pair).sync(); sequence in destroyPoolToken().
The public pre-state before block 37914434 already contained the live SATX/WBNB pool, verified SATX bytecode, and public Pancake liquidity needed for the strategy. The attacker did not need any privileged state or hidden contract behavior. The funder EOA sent 0.900000001 BNB to the helper contract and invoked the exploit entrypoint, which is visible in the seed metadata and trace.
The helper first bought a small amount of SATX and added liquidity. This was not the profit step; it primed SATX's LP-tracking logic so later pair-facing transfers followed the intended code path. The trace then shows a flash swap of 60 WBNB from the public CAKE/WBNB pair, followed by a nested SATX/WBNB flash swap that withdrew roughly half of the pair's SATX reserve.
The decisive state corruption happened when the helper transferred SATX back to the SATX/WBNB pair. That transfer entered SATX's to == uniswapV2Pair branch, which invoked destroyPoolToken(). The token contract burned pair inventory and forced sync() from inside the transfer path, so the pair's reserve accounting was rewritten around an intermediate, token-manipulated state rather than the economic end state of the swap.
The seed trace shows the exact exploitation sequence:
0x0eD7...14fD0::swap(... 60000000000000000000, attackerHelper, 0x31)
attackerHelper::pancakeCall(...)
PancakePair::swap(100000000000000, 350018558642186154111639, attackerHelper, 0x31)
SATX::transfer(PancakePair, 339553003738784788102111)
PancakePair::skim(attackerHelper)
PancakePair::sync()
Immediately after the manipulated transfer, skim() returned a massive SATX surplus to the helper. The trace shows PancakePair::skim(...) transferring 329400368926995122935810 SATX back to the helper, while the subsequent sync() updated reserves down to only 3465183730557642929408 SATX. That is the reserve/balance divergence becoming attacker-controlled inventory.
The attacker then sold the recovered SATX back into WBNB. The trace shows the SATX/WBNB pair's WBNB balance falling to 1070439732107143399 wei, confirming severe reserve collapse. The balance diff corroborates the pool depletion: the pair lost 367171977564595082809247 SATX and the profit recipient EOA gained 50239562082329246313 wei, while the funder EOA lost the seed plus gas. This matches the claimed net profit of 49336934003329246313 wei.
The adversary cluster consists of:
0xbef24b94c205999ea17d2ae4941ce849c9114bfd: funding EOA and caller of the helper.0x9c63d6328c8e989c99b8e01de6825e998778b103: helper contract that executed the flash-swap sequence.0x98c74b8686eaa050d89cfc9757160ec1223ce095: profit recipient EOA.The transaction-level flow was:
0.900000001 BNB to the helper and called its exploit entrypoint.60 WBNB from the public CAKE/WBNB pair.destroyPoolToken(), which burned pair inventory and called sync() from SATX token code.skim() to pull the now-surplus SATX back out of the pair.50.239562082329246313 BNB to the profit recipient.The helper contract used an owner check, but that owner check is part of the adversary's own orchestration. It does not limit strategy-level reproducibility because any unprivileged actor could deploy an equivalent helper and call the same public pairs and router against the same public pre-state.
The exploit drained value from the SATX/WBNB market rather than from a privileged treasury. The primary measurable loss is 49.336934003329246313 BNB of attacker profit realized from the manipulated pool state.
The concrete loss summary is:
WBNB4933693400332924631318The pair's WBNB side was effectively collapsed from about 50.56 WBNB before the exploit sequence to about 1.07 WBNB after the final exit swap, demonstrating severe liquidity depletion and price dislocation for SATX market participants.
0x7e02ee7242a672fb84458d12198fae4122d7029ba64f3673e7800d811a8de93f on BSC block 379144340xfd80a436da2f4f4c42a5dbfa397064cfeb7d95080x927d7adf1bcee0fa1da868d2d43417ca7c6577d40x0ed7e52944161450477ee417de9cd3a859b14fd0destroyPoolToken() and pair-facing _transfer() logicskim() and sync()skim(), sync(), exit swap, and final BNB payout