All incidents

DKP Exchange Flash-Price Exploit

Share
Mar 08, 2023 10:16 UTCAttackLoss: 78,559.55 USDTPending manual check2 exploit txWindow: 42s
Estimated Impact
78,559.55 USDT
Label
Attack
Exploit Tx
2
Addresses
2
Attack Window
42s
Mar 08, 2023 10:16 UTC → Mar 08, 2023 10:17 UTC

Exploit Transactions

TX 1BSC
0x0c850f54c1b497c077109b3d2ef13c042bb70f7f697201bcf2a4d0cb95e74271
Mar 08, 2023 10:16 UTCExplorer
TX 2BSC
0x2d31e45dce58572a99c51357164dc5283ff0c02d609250df1e6f4248bd62ee01
Mar 08, 2023 10:17 UTCExplorer

Victim Addresses

0x89257a52ad585aacb1137fcc8abbd03a963b9683BSC
0xbe654fa75bad4fd82d3611391fda6628bb000cc7BSC

Loss Breakdown

78,559.55USDT

Similar Incidents

Root Cause Analysis

DKP Exchange Flash-Price Exploit

1. Incident Overview TL;DR

On BSC, the adversary cluster used two transactions to extract value from the unverified DKP exchange contract at 0x89257A52Ad585Aacb1137fCc8abbD03a963B9683. In tx 0x0c850f54c1b497c077109b3d2ef13c042bb70f7f697201bcf2a4d0cb95e74271 at block 26284132, the attacker flash-borrowed 259390 USDT from the DKP/USDT Pancake pair 0xBE654FA75bAD4Fd82D3611391fDa6628bB000CC7, temporarily depressed the pair's live USDT balance, and bought DKP from the exchange for only 100 USDT while the exchange was mispricing inventory from those manipulated balances. In tx 0x2d31e45dce58572a99c51357164dc5283ff0c02d609250df1e6f4248bd62ee01 at block 26284146, the attacker sold the acquired DKP back through PancakeRouter and realized 79233.963143957652842113 USDT to the EOA.

The root cause is a deterministic pricing bug, not a privileged action. The exchange's getUsdtPrice() logic prices DKP as DKP.balanceOf(lp) * 1e18 / USDT.balanceOf(lp), so a flash-loan-induced reduction in the pair's live USDT balance makes DKP appear massively cheaper than the honest market price. A constructor-time helper contract bypassed the exchange's contract-caller gate because extcodesize(msg.sender) is zero during construction, allowing the manipulated quote to be consumed permissionlessly. Across the two transactions, the adversary cluster's combined USDT balance increased by 78459.549143957652842113, while the pair lost 78559.549143957652842113 USDT and the exchange lost 17115.243270155346986000 DKP gross inventory.

2. Key Background

  • The primary victim-side component is the unverified exchange contract 0x89257A52Ad585Aacb1137fCc8abbD03a963B9683. Validator storage reads at block 26284131 show that it points at the DKP/USDT Pancake pair in slot 1, USDT in slot 2, DKP in slot 3, uses a 1000 fee denominator in slot 4, fixed shares 30/10/5/5 in slots 5 to 8, and a minimum trade amount of 60e18 in slot 9.
  • The DKP token at 0xd06fa1ba7c80f8e113c2dc669a23a9524775cf19 is verified source. Its _transfer path applies fees on AMM interactions, which is why the tx2 sale sends only 16178.183701114341738517 DKP into the pair even though the attacker starts the second transaction with 17029.667053804570251070 DKP.
  • The DKP/USDT Pancake pair at 0xBE654FA75bAD4Fd82D3611391fDa6628bB000CC7 is a standard UniswapV2-style pair. Its live ERC-20 balances can diverge from getReserves() inside a flash-swap callback before the transaction settles, which is exactly the state the exchange incorrectly trusts.

3. Vulnerability Analysis & Root Cause Summary

This incident is an ATTACK against the exchange contract's pricing logic. The exchange does not use a manipulation-resistant oracle, a TWAP, or even the pair's stored reserves; instead it prices DKP from raw token balances currently sitting in the LP address. That choice makes the quote directly controllable inside a single transaction with a flash swap. The tx1 trace shows the exchange reading DkpToken::balanceOf(pair) and BEP20USDT::balanceOf(pair) after the pair has already transferred out 259390 USDT, so the quote is based on 36873.976322434846364457 DKP versus only 215.445236391899433885 USDT. Under honest pre-state balances, 100 USDT would buy about 14.203853193009140470 DKP; under the manipulated state, the exchange computes 17115.243270155346986000 DKP gross for the same 100 USDT input and pays 17029.667053804570251070 DKP net after the fixed connection share. The exchange's anti-bot control is also ineffective, because a freshly created helper contract can call exchange(uint256) from its constructor while its runtime code size is still zero.

The vulnerable components are therefore:

  • 0x89257A52Ad585Aacb1137fCc8abbD03a963B9683::getUsdtPrice() using live LP balances as a price oracle.
  • 0x89257A52Ad585Aacb1137fCc8abbD03a963B9683::exchange(uint256) multiplying user input by that manipulable quote and transferring real DKP inventory.

The violated security principles are straightforward: do not price inventory from same-tx manipulable LP balances, do not rely on msg.sender.code.length for authorization, and do not treat a single-pool spot ratio as a safe oracle for inventory sales.

4. Detailed Root Cause Analysis

The pre-state at block 26284131 already contains everything needed for the exploit: the DKP/USDT pair is liquid, the exchange still holds enough DKP to sell, and its minimum trade configuration is active.

Validator storage reads at block 26284131
exchange.slot1 = 0x...be654fa75bad4fd82d3611391fda6628bb000cc7  (LP pair)
exchange.slot2 = 0x...55d398326f99059ff775485246999027b3197955  (USDT)
exchange.slot3 = 0x...d06fa1ba7c80f8e113c2dc669a23a9524775cf19  (DKP)
exchange.slot4 = 1000
exchange.slot5 = 30
exchange.slot6 = 10
exchange.slot7 = 5
exchange.slot8 = 5
exchange.slot9 = 60000000000000000000  (minimum trade = 60e18)
pair USDT = 259605445236391899433885
pair DKP  = 36873976322434846364457
exchange DKP = 17901400217105153011279

Tx 0x0c850f54... begins with attacker contract 0xf34ad6cea329f62f4516ffe00317ab09d934fba3 calling PancakePair::swap(259390e18, 0, ...). During the callback, the attacker transfers 100e18 USDT to a fresh helper 0xb24fc2f9ee4467cf64990584fab02274aa247735, deploys it, approves the exchange, and calls exchange(100e18) from the helper's constructor.

Collector trace for tx 0x0c850f54...
PancakePair::swap(259390000000000000000000, 0, 0xf34a..., 0x00)
  0xf34a...::pancakeCall(...)
    BEP20USDT::transfer(0xb24f..., 100000000000000000000)
    new helper @0xb24f...
      BEP20USDT::approve(exchange, 100000000000000000000)
      exchange::exchange(100000000000000000000)
        DkpToken::balanceOf(pair) -> 36873976322434846364457
        BEP20USDT::balanceOf(pair) -> 215445236391899433885
        DkpToken::transfer(0xb24f..., 17029667053804570251070)

Those two balanceOf calls are the decisive breakpoint. The pair still reports the honest reserves through getReserves(), but the exchange does not use them. Instead it sees only 215.445236391899433885 USDT still sitting in the pair address after the flash borrow, computes a massively inflated DKP-per-USDT ratio, and transfers out real exchange inventory. The economics are explicit:

  • Honest pre-state quote: 36873.976322434846364457 / 259605.445236391899433885 = 0.1420385319300914047034 DKP per USDT.
  • Manipulated same-tx quote: 36873.976322434846364457 / 215.445236391899433885 = 171.1524327015534698603 DKP per USDT.
  • For 100 USDT, the honest output would be about 14.203853193009140470 DKP, while the manipulated quote produces 17115.243270155346986000 DKP gross and 17029.667053804570251070 DKP net after the fixed share transfer.

The constructor-time helper matters because the exchange rejects normal deployed contract callers. The tx1 trace embeds the helper constructor bytecode and shows the call succeeding before runtime code exists at 0xb24f..., which is consistent with the known extcodesize construction bypass.

After the underpriced purchase, the attacker repays the flash swap with the borrowed USDT plus 674.414 USDT from its own working capital, for a total pair inflow of 260064.414 USDT. That ends tx1 with the attacker contract holding 17029.667053804570251070 DKP and the exchange contract down by 17115.243270155346986000 DKP gross.

Tx 0x2d31e45d... realizes the profit. The attacker approves PancakeRouter and sells the DKP into the same pair.

Collector trace for tx 0x2d31e45d...
0xf34a...::11a73c8e()
  DkpToken::balanceOf(0xf34a...) -> 17029667053804570251070
  PancakeRouter::swapExactTokensForTokensSupportingFeeOnTransferTokens(
    17029667053804570251070,
    0,
    [DKP, USDT],
    0xF38B677fa6E9E51338D0c32FD21afe43406E06Df,
    1678270651
  )
    DkpToken::transferFrom(0xf34a..., pair, 17029667053804570251070)
    emit Transfer(... to pair, value: 16178183701114341738517)
    PancakePair::swap(79233963143957652842113, 0, attacker EOA, 0x)

The verified DKP token source explains the sale-side fee: only 16178.183701114341738517 DKP reaches the pair, and the rest is distributed according to the token's fee schedule. Even with that fee, the sale drains 79233.963143957652842113 USDT from the pair to attacker EOA 0xf38b677fa6e9e51338d0c32fd21afe43406e06df. RPC balance checks at block 26284146 confirm the final state reported in the JSON artifacts: the EOA holds 79233.963143957652842113 USDT, the attacker contract still holds 158.331305403783296414 USDT, and the pair's USDT balance has fallen to 181045.896092434246591772.

5. Adversary Flow Analysis

The adversary cluster contains:

  • EOA 0xf38b677fa6e9e51338d0c32fd21afe43406e06df, the sender of both attacker-crafted transactions and the final USDT profit recipient.
  • Contract 0xf34ad6cea329f62f4516ffe00317ab09d934fba3, which performs the flash swap in tx1 and the sale in tx2.
  • Constructor-time helper 0xb24fc2f9ee4467cf64990584fab02274aa247735, deployed during tx1 only to satisfy the exchange's weak caller gate.

The end-to-end flow is:

  1. Price manipulation and purchase, tx 0x0c850f54... / block 26284132: the attacker flash-borrows 259390 USDT from the pair, deploys the helper during the callback, pays 100 USDT into the exchange while the pair balance is depressed, and receives 17029.667053804570251070 DKP.
  2. Monetization, tx 0x2d31e45d... / block 26284146: after waiting 14 blocks, the attacker sells the DKP through PancakeRouter back into the same pair and receives 79233.963143957652842113 USDT to the EOA.
  3. Net outcome: the adversary cluster's combined USDT rises from 932.745305403783296414 to 79392.294449361436138527, a net gain of 78459.549143957652842113 USDT before native gas, while the pair loses 78559.549143957652842113 USDT and the exchange loses gross DKP inventory.

The exploit remains ACT because every step is permissionless: flash swaps are public, the exchange sale path is public, the constructor-time helper is locally deployable by any actor, and the follow-up sale uses public router liquidity.

6. Impact & Losses

The measurable protocol-side loss is concentrated in the DKP/USDT Pancake pair's USDT reserves. From the start of the sequence to the end of tx2, the pair loses 78559.549143957652842113 USDT, represented in smallest units as 78559549143957652842113. The exchange contract also transfers out 17115.243270155346986000 DKP gross inventory on a payment of only 100 USDT, which is the direct manifestation of the pricing bug. The adversary cluster realizes 78459.549143957652842113 USDT profit before gas, while the remaining 100 USDT is the exchange payment path and the difference versus the pair depletion is explained by the attacker's residual contract balance.

7. References

  • Seed index covering both exploit transactions and collected artifacts.
  • Tx 0x0c850f54c1b497c077109b3d2ef13c042bb70f7f697201bcf2a4d0cb95e74271 metadata, balance diff, and full trace.
  • Tx 0x2d31e45dce58572a99c51357164dc5283ff0c02d609250df1e6f4248bd62ee01 metadata, balance diff, and full trace.
  • Verified DKP token source at 0xd06fa1ba7c80f8e113c2dc669a23a9524775cf19, especially _transfer, _initParam, and _takeFee.
  • Validator RPC reads at block 26284131 confirming exchange storage slots and token balances used in the analysis.