STV Sell Accounting Drain
Exploit Transactions
0xf2a0c957fef493af44f55b201fbc6d82db2e4a045c5c856bfe3d8cb80fa30c12Victim Addresses
0x2120f8f305347b6aa5e5dbb347230a8234eb3379BSCLoss Breakdown
Similar Incidents
Keep Rising Public Sell LP Drain
37%CS Pair Balance Burn Drain
35%NeverFallToken LP Drain
35%GGGTOKEN Treasury Drain via receive()
34%Cellframe Migration Drain
34%ZS Pair Burn Drain
33%Root Cause Analysis
STV Sell Accounting Drain
1. Incident Overview TL;DR
The BNB Chain STV market at 0x2120f8f305347b6aa5e5dbb347230a8234eb3379 was drained through a public, flash-loan-assisted trading sequence. An attacker-controlled helper borrowed USDT from DODO, bought STV twice, sold STV twice, repaid the flash loan principal, and still retained 397782460903618735050562 USDT plus leftover STV.
The root cause is a sell-side accounting bug in sell(uint256). The contract prices the seller's full STV input using getPrice(), but then transfers 50% of that same STV to the fee sink returned by white(). The market therefore pays USDT for STV it does not retain.
2. Key Background
The victim market is unverified, but its ABI and disassembly show the public entrypoints buy(uint256), sell(uint256), getPrice(), and white(). The seed transaction targeted the attacker helper 0x1c651b04bdd1c1718eeeafede0a3c13e87024fa2, which had been deployed earlier by EOA 0x4b44b52ef15f7aab9a0a6cfdfa8d2c5eeeb6d8c3.
getPrice() is reserve-ratio pricing. Before the exploit, the market held 521020643056518743156196 USDT and quoted 54302860976874580, derived from on-contract USDT and STV balances. The buy path also pays USDT fees to white(), while the sell path pays both STV and USDT fees to the same sink, which matters because the sell-side pricing is computed before those fee side effects are fully accounted for.
3. Vulnerability Analysis & Root Cause Summary
This is an attack-class accounting flaw in the victim market's public sell path. The market computes gross USDT proceeds from the full seller-supplied STV amount at the current reserve ratio. It then diverts half of the sold STV to white() as a fee instead of first reducing the economically priced amount. After that, it still transfers 90% of the gross USDT to the seller and 10% of the gross USDT to white(). The result is that the pool's USDT reserve falls as if it had received the full STV amount even though it keeps only half. Because the entrypoints are public and flash liquidity is public, any unprivileged actor can realize this imbalance permissionlessly.
4. Detailed Root Cause Analysis
The victim contract summary identifies getPrice() and sell(uint256) as callable selectors on the exploited market. The disassembly-based analysis and the execution trace line up with the following mechanism:
getPrice() = USDT.balanceOf(market) * 1e18 / STV.balanceOf(market)
sell(amount):
grossUsdt = amount * getPrice() / 1e18
stvFee = amount * 50 / 100
usdtFee = grossUsdt * 10 / 100
transfer STV fee to white()
transfer 90% of grossUsdt to seller
transfer 10% of grossUsdt to white()
The seed trace shows the first sell calling:
0x2120...3379::sell(1502483458493195656785812)
STV::transfer(0xF037...0B04, 751241729246597828392906)
USDT::transfer(attacker helper, 568049363438574320900247)
USDT::transfer(0xF037...0B04, 63116595937619368988916)
Those transfers prove the defect. The market first accepted 1502483458493195656785812 STV from the seller, then forwarded exactly half of it, 751241729246597828392906, to white(), while also paying out the gross-USDT-derived proceeds. The second sell repeats the same broken pattern on a larger amount. The seed balance diff then confirms the macro effect: the attacker helper ended with 397782460903618735050562 more USDT, the market lost 517684520469819705280497 USDT, and the fee sink gained both STV and USDT.
5. Adversary Flow Analysis
The exploit path is fully public and end to end:
- EOA
0x4b44b52ef15f7aab9a0a6cfdfa8d2c5eeeb6d8c3deployed helper0x1c651b04bdd1c1718eeeafede0a3c13e87024fa2in tx0xce0e119a317a9e67f37f67e281c546b2cf7df3d4f37e4a660faf245461d49134. - In seed tx
0xf2a0c957fef493af44f55b201fbc6d82db2e4a045c5c856bfe3d8cb80fa30c12, the helper borrowed600296101490916296932929USDT from DODO pool0xfeafe253802b77456b4627f8c2306a9cebb5d681. - The helper called
buy(300148050745458148466464)twice, converting the borrowed USDT into STV through the victim market. - The helper called
sell(1502483458493195656785812)andsell(3375520392347497456166643), triggering the broken sell-side accounting twice. - The helper repaid the flash-loaned principal and retained the remaining USDT and STV as profit.
The trace excerpt shows the core execution skeleton:
0xFeAF...d681::flashLoan(..., 600296101490916296932929, 0x1c65...4fa2, ...)
0x2120...3379::buy(300148050745458148466464)
0x2120...3379::buy(300148050745458148466465)
0x2120...3379::sell(1502483458493195656785812)
0x2120...3379::sell(3375520392347497456166643)
6. Impact & Losses
The victim market lost 517684520469819705280497 USDT smallest units in the seed transaction. With decimal = 18, that corresponds to 517684.520469819705280497 USDT. The attacker helper realized 397782460903618735050562 USDT of net post-repayment profit and also retained 2068867337245240376360201 STV. The fee sink 0xF037A9F7e7eE68A011AB3E4FA8667DD350370B04 also gained both STV and USDT during the exploit flow. The reserve-ratio price collapsed from 54302860976874580 before exploitation to roughly 655832853278045 afterward, showing severe solvency damage.
7. References
- Seed exploit tx:
0xf2a0c957fef493af44f55b201fbc6d82db2e4a045c5c856bfe3d8cb80fa30c12 - Helper deployment tx:
0xce0e119a317a9e67f37f67e281c546b2cf7df3d4f37e4a660faf245461d49134 - Victim market:
0x2120f8f305347b6aa5e5dbb347230a8234eb3379 - Attacker helper:
0x1c651b04bdd1c1718eeeafede0a3c13e87024fa2 - DODO flash-loan pool:
0xfeafe253802b77456b4627f8c2306a9cebb5d681 - Fee sink
white():0xf037a9f7e7ee68a011ab3e4fa8667dd350370b04 - Evidence used: seed trace, seed balance diff, victim contract summary/disassembly, and helper transaction history