Calculated from recorded token losses using historical USD prices at the incident time.
0xd03702e17171a32464ce748b8797008d59e2dbcecd3b3847d5138414566c886d0x3da4828640ad831f3301a4597821cc3461b06678BSC0xa2633ca9eb7465e7db54be30f62f577f039a2984BSCOn BNB Chain block 37680755, exploit transaction 0xd03702e17171a32464ce748b8797008d59e2dbcecd3b3847d5138414566c886d drained the UPS/USDT Pancake pair at 0xa2633ca9eb7465e7db54be30f62f577f039a2984. The adversary EOA 0xc4c306c20b4a22f1d261f11c40e88603dd2a2888 invoked helper contract 0xf5d943805284bfa9c58eb3d777dd5791b5f9da10, borrowed public flash liquidity, bought UPS, repeatedly collapsed the pair’s recorded UPS reserve with UPS.transfer(pair, amount) plus PancakePair.skim, then used tiny sells and PancakePair.swap calls to extract nearly all USDT from the pair.
The root cause is a token-side logic flaw in UPS 0x3da4828640ad831f3301a4597821cc3461b06678. In the sell branch of UPS._update, the token burns amount - fee from the pair itself and immediately calls pair.sync() before the seller’s transfer is credited back to the pair. That breaks the expected AMM invariant that the pair’s reserve for the sold token must not decrease before the pair accounts for seller input and computes output. Because the actual UPS balance then exceeds the stored reserve, the attacker can recover the excess with skim, repeat the process until the stored UPS reserve is near zero, and then drain USDT at a grossly distorted price.
The affected liquidity venue is a PancakeSwap V2 style AMM pair holding UPS and USDT. In this model, reserve accounting is maintained by the pair contract, and swap pricing assumes the token transfer into the pair does not arbitrarily mutate reserves before the pair computes and .
amountInamountOutTwo Pancake behaviors matter here:
sync() sets stored reserves to the pair’s current token balances.skim(to) transfers any token balance above the stored reserve to to.UPS adds custom transfer-hook behavior on top of normal ERC20 movement. Its constructor creates the pair against USDT and its _update override applies special logic whenever either side of the transfer is the pair. That means the token contract, not just the pair, can affect the pair’s balance and reserve accounting during trades.
The collected evidence also shows the UPS/USDT pair itself is a verified PancakePair deployment on BscScan, matching the standard Pancake pair interface and reserve-management behavior used in the exploit.
The vulnerability class is an unsafe token transfer hook that mutates AMM state during sells. In the UPS sell path, to == pairAddress, the token computes a 5% fee and then calls _swapBurn(amount - fee). That helper executes super._burn(pairAddress, amount) followed by ISwapPair(pairAddress).sync(). Only after those operations does UPS call super._update(from, to, amount - fee) to credit the seller’s net tokens back to the pair.
This ordering is the critical flaw. During a legitimate sell, the pair’s UPS reserve should not fall before the pair accounts for the seller’s input. UPS instead reduces the pair’s UPS balance, synchronizes the lower balance into stored reserves, and only afterward transfers the seller’s net amount to the pair. The result is a persistent mismatch where the pair’s actual UPS balance is greater than the recorded reserve. Because skim transfers any such excess out, an attacker can immediately reclaim that difference and keep the artificially lowered reserve in place.
The exploit does not rely on privileged access, private attacker artifacts, or hidden state. Any unprivileged actor can obtain UPS, call UPS.transfer(pair, amount), call skim, and repeat. Once the stored UPS reserve is pushed near zero while the pair still holds USDT, a very small additional UPS input produces an outsized USDT output through the standard Pancake V2 pricing formula. That is the code-level root cause and the deterministic ACT opportunity.
The relevant UPS source is the verified contract collected for 0x3da4828640ad831f3301a4597821cc3461b06678. The vulnerable logic is:
function _swapBurn(uint amount) private lockTheSwap {
super._burn(pairAddress, amount);
ISwapPair(pairAddress).sync();
}
function _update(address from, address to, uint256 amount) internal virtual override {
if (inSwapAndLiquify || whiteMap[from] || whiteMap[to] || !(from == pairAddress || to == pairAddress)) {
super._update(from, to, amount);
} else if (from == pairAddress) {
require(canBuy || getPrice() > 5e14, "can not trade");
if (!canBuy) {
canBuy = true;
}
super._update(from, to, amount);
} else if (to == pairAddress) {
uint256 fee = amount * 5 / 100;
if (!inSwapAndLiquify) {
_swapBurn(amount - fee);
}
...
super._update(from, to, amount - fee);
}
}
This establishes the broken invariant:
For a sell into the UPS/USDT pair, the pair's UPS reserve must not decrease
before the pair accounts for seller input and computes swap output.
The exploit transaction trace shows the invariant break in practice. The helper first borrowed 3500000000000000000000000 USDT from flash source 0x4f31fa980a675570939b737ebdde0471a4be40eb, transferred 2000000000000000000000000 USDT to the pair and synced it, then routed 1000000000000000000000000 USDT through PancakeRouter to buy 781100330303169117310636451 UPS:
0x4f31Fa980a675570939B737Ebdde0471a4Be40Eb::flash(..., 3500000000000000000000000, ...)
...
BEP20USDT::transfer(PancakePair, 2000000000000000000000000)
PancakePair::sync()
...
PancakeRouter::swapExactTokensForTokensSupportingFeeOnTransferTokens(
1000000000000000000000000, 0, [USDT, UPS], attacker, ...
)
PancakePair::swap(781100330303169117310636451, 0, attacker, 0x)
After that acquisition, the pair held 1589826007352123383157266307 UPS. The attacker then entered the reserve-collapse loop. In each cycle, UPS.transfer(pair, amount) triggered the sell branch above. The trace shows that the token emitted a burn from the pair, then PancakePair::sync() recorded the lower UPS balance, and finally the attacker reclaimed the balance-reserve mismatch through skim. Repeating that process drove the pair’s UPS balance and recorded reserve down to 111591910519923901:
UPS::transfer(PancakePair, 781100330303169117310636451)
emit Transfer(from: PancakePair, to: 0x0000000000000000000000000000000000000000, value: 742045313788010661445104629)
PancakePair::sync()
...
PancakePair::skim(attacker)
...
UPS::balanceOf(PancakePair) -> 111591910519923901
The balance diff confirms the end state created by this sequence:
{
"token": "0x3da4828640ad831f3301a4597821cc3461b06678",
"holder": "0xa2633ca9eb7465e7db54be30f62f577f039a2984",
"before": "2370926337655292500467902758",
"after": "111591910519923901",
"delta": "-2370926337543700589947978857"
}
With reserve0 nearly zero, the attacker needed only tiny additional UPS transfers to obtain enormous USDT outputs from PancakePair.swap. Near the end of the trace, the pair reserves were 5579595525996195 UPS and 19120238699404809722 USDT before one final small sell produced 18161950830746505772 USDT. After the draining swaps, the pair’s USDT balance fell to 958287868658303950, the flash source received principal plus fee, and the helper retained 28527843051551135057044 USDT profit:
{
"token": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0xa2633ca9eb7465e7db54be30f62f577f039a2984",
"before": "30278801339419793360994",
"after": "958287868658303950",
"delta": "-30277843051551135057044"
}
{
"token": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0x4f31fa980a675570939b737ebdde0471a4be40eb",
"before": "15924190631187428753720247",
"after": "15925940631187428753720247",
"delta": "1750000000000000000000"
}
{
"token": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0xf5d943805284bfa9c58eb3d777dd5791b5f9da10",
"before": "0",
"after": "28527843051551135057044",
"delta": "28527843051551135057044"
}
The full exploit was realized inside one adversary-crafted transaction:
0xc4c306c20b4a22f1d261f11c40e88603dd2a2888 called helper contract 0xf5d943805284bfa9c58eb3d777dd5791b5f9da10.3,500,000 USDT via the public flash interface at 0x4f31fa980a675570939b737ebdde0471a4be40eb.2,000,000 USDT into the UPS/USDT pair and called sync() to re-anchor reserves.0x10ed43c718714eb63d5aa57b78b54704e256024e and swapped 1,000,000 USDT for UPS.PancakePair.skim(attacker) to recover the excess UPS balance that was no longer reflected in the stored reserve.swap calls to pull out almost all USDT from the pair.1750000000000000000000 USDT fee.28527843051551135057044 USDT and 697425276732550013519578201 UPS.The transaction is ACT because every dependency is public and permissionless: the flash source, the pair, the router, the verified UPS code, the pair reserves, and the exploit transaction shape are all observable from canonical on-chain data.
The measurable loss is the USDT drained from the UPS/USDT Pancake pair:
30277843051551135057044 smallest units with decimal=18958287868658303950285278430515511350570441750000000000000000000The pair also lost effective UPS-side price integrity because the recorded UPS reserve was collapsed from 2370926337655292500467902758 to 111591910519923901, enabling the final mispriced swaps.
0xd03702e17171a32464ce748b8797008d59e2dbcecd3b3847d5138414566c886d0xd03702e17171a32464ce748b8797008d59e2dbcecd3b3847d5138414566c886d0xd03702e17171a32464ce748b8797008d59e2dbcecd3b3847d5138414566c886d0x3da4828640ad831f3301a4597821cc3461b066780xa2633ca9eb7465e7db54be30f62f577f039a2984