Calculated from recorded token losses using historical USD prices at the incident time.
0xff5515268d53df41d407036f547b206e288b226989da496fda367bfeb31c5b8b0x1afa48b74ba7ac0c3c5a2c8b7e24eb71d440846fBSC0x5f739a4ade4341d4aee049e679095bccbe904ee1BSCOn BNB Chain transaction 0xff5515268d53df41d407036f547b206e288b226989da496fda367bfeb31c5b8b in block 28864174, an unprivileged adversary used a public DODO flash loan to buy UN, repeatedly manipulated the UN/USDT pair reserve accounting, then exited back to USDT at an inflated price. The attacker repaid the flash loan inside the same transaction and realized 26558.707032043241953536 USDT of profit.
The root cause is in the UN token itself. Any public transfer to swapPair enters a pre-settlement hook that burns pair-held UN and immediately calls sync() before the sell transfer finishes. Because the later transfer lands after the reserve sync, the attacker can call skim(address) on the pair to recover the post-sync excess and repeat the loop until the pair's UN reserve is heavily depressed.
UN is the token at 0x1afa48b74ba7ac0c3c5a2c8b7e24eb71d440846f. Its configured swapPair is the UN/USDT pair at 0x5f739a4ade4341d4aee049e679095bccbe904ee1. The pair is not verified on Etherscan, but the saved source lookup shows it is unverified rather than unknown, and the execution trace directly proves that it exposes the public sync() and skim(address) entrypoints used in the exploit.
The exploit relies on standard Uniswap V2 semantics. sync() updates stored reserves to current balances, while skim(address) transfers any balance above stored reserves to an arbitrary recipient. Under normal AMM accounting, arbitrary traders should not be able to change the pair's reserves except through value-conserving swap or LP flows.
The attacker also used DODO pool 0xfeafe253802b77456b4627f8c2306a9cebb5d681 as a public flash-loan source. No privileged access, private calldata, or attacker-specific infrastructure was required beyond deploying a helper contract and composing public calls in one transaction.
The vulnerability class is an application-level attack caused by unsafe reserve mutation inside the token contract. UN places a public hook on every sell into swapPair, and that hook runs before the sell-side transfer accounting is complete. Inside the hook, UN burns a caller-controlled amount of pair inventory and calls sync(), which resets the pair's stored reserve to the reduced UN balance. When control returns to the sell path, UN transfers roughly 90.5% of the caller's amount into the pair, but that balance is now surplus relative to the just-synced reserves. Because the pair still exposes public skim(address), the attacker can immediately reclaim the surplus. Repeating this pattern lets the attacker pay taxes while driving down the pair's synced UN reserve. Once the reserve is sufficiently low, the attacker can sell the remaining UN into a badly distorted pool price and extract USDT.
The violated invariant is straightforward: a public transfer into the pair must not let an arbitrary seller destroy pair inventory, resync the reserve baseline, and reclaim the later transfer as excess. The precise breakpoint is UN's _transfer() function calling _swapBurn(amount) when to == swapPair, and _swapBurn(amount) executing a pair burn plus sync() before the sell settles.
The verified UN source shows the bug directly:
function _transfer(address from, address to, uint256 amount) internal override {
...
if (swapPair != address(0) && to == swapPair && !_inSwapAndLiquify) {
_swapBurn(amount);
}
...
} else if (to == swapPair) {
uint256 every = amount.div(1000);
...
super._transfer(from, to, amount - every * 95);
}
}
function _swapBurn(uint amount) private lockTheSwap {
if (balanceOf(address(this)) > 0)
super._transfer(address(this), swapPair, balanceOf(address(this)));
if (totalCirculation() > minTotalSupply() + (amount * 40) / 100) {
super._burn(swapPair, (amount * 40) / 100);
}
ISwapPair(swapPair).sync();
}
This code establishes the full exploit path:
amount by calling UN.transfer(pair, amount)._swapBurn(amount) burns 40% of amount from the pair's existing UN inventory._swapBurn(amount) immediately calls ISwapPair(swapPair).sync(), so the pair stores the post-burn UN balance as its new reserve.amount - every * 95 into the pair.skim(address).The on-chain trace shows this sequence repeating in the live exploit. The transaction begins with a DODO flash loan and the initial UN purchase:
0xFeAFe253802b77456B4627F8c2306a9CeBb5d681::flashLoan(..., 29100000000000001048576, ...)
0x5F739a4AdE4341D4AEe049E679095BcCbe904Ee1::swap(91391982773176450879376, 0, attacker, 0x)
The reserve-manipulation loop is then visible several times:
UN::transfer(pair, 84994543979054099317825)
0x5F739a4AdE4341D4AEe049E679095BcCbe904Ee1::sync()
0x5F739a4AdE4341D4AEe049E679095BcCbe904Ee1::skim(attacker)
...
UN::transfer(pair, 71535657939970882690921)
0x5F739a4AdE4341D4AEe049E679095BcCbe904Ee1::sync()
0x5F739a4AdE4341D4AEe049E679095BcCbe904Ee1::skim(attacker)
...
UN::transfer(pair, 30103993252588246708450)
0x5F739a4AdE4341D4AEe049E679095BcCbe904Ee1::sync()
0x5F739a4AdE4341D4AEe049E679095BcCbe904Ee1::skim(attacker)
After these loops collapse the pair's synced UN reserve, the attacker performs the final manipulated exit:
0x5F739a4AdE4341D4AEe049E679095BcCbe904Ee1::swap(
0,
55658707032043243002112,
attacker,
0x
)
BEP20USDT::transfer(DODO_pool, 29100000000000001048576)
BEP20USDT::transfer(sender, 26558707032043241953536)
The balance diff confirms the outcome quantitatively. The pair lost 26558707032043241953536 raw USDT units and the seed EOA gained exactly the same amount, while also paying 582331659769200000 wei of gas:
{
"pair_usdt_delta": "-26558707032043241953536",
"attacker_usdt_delta": "26558707032043241953536",
"attacker_gas_delta_wei": "-582331659769200000"
}
The exploit is therefore deterministic and ACT-feasible from public state alone:
swapPair is publicly configured.sync() and skim(address) behavior is proven by the seed trace.The adversary cluster consists of:
0xf84efa8a9f7e68855cf17eaac9c2f97a9d131366, which submitted the transaction and received the final profit.0x98e241bd3be918e0d927af81b430be00d86b04f9, which executed the flash-loan callback logic on-chain.The execution flow is:
29100 USDT from DODO pool 0xfeafe253802b77456b4627f8c2306a9cebb5d681.91391982773176450879376 UN.UN.transfer(pair, amount) followed by pair.skim(attacker):
_swapBurn(amount),_swapBurn burns pair-held UN and calls sync(),skim(attacker) recovers the post-sync excess.55658707032043243002112 raw USDT units.26558707032043241953536 raw USDT units to the seed EOA.This is a single-transaction ACT realization. The helper contract only packages public actions; it does not add any privileged capability beyond orchestration.
The direct measurable loss is the USDT drained from the UN/USDT pair:
USDT265587070320432419535361826558.707032043241953536The pool also suffered severe reserve distortion on the UN side. The balance diff shows the pair's UN balance dropping from 186912581853851065964787 to 53382130893876527157390, a reduction of 133530450959974538807397 raw UN units. The transaction therefore caused both immediate LP loss in USDT terms and a large reserve collapse that enabled the inflated exit price.
0xff5515268d53df41d407036f547b206e288b226989da496fda367bfeb31c5b8btransfer -> sync -> skim loops, final swap, repayment, and profit transfer.0x1afa48b74ba7ac0c3c5a2c8b7e24eb71d440846f.0x5f739a4ade4341d4aee049e679095bccbe904ee1, showing the pair is unverified rather than unknown.