All incidents

DFS Pair Accounting Drain

Share
Dec 30, 2022 10:59 UTCAttackLoss: 1,461.8 USDTPending manual check1 exploit txWindow: Atomic
Estimated Impact
1,461.8 USDT
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Dec 30, 2022 10:59 UTC → Dec 30, 2022 10:59 UTC

Exploit Transactions

TX 1BSC
0xcddcb447d64c2ce4b3ac5ebaa6d42e26d3ed0ff3831c08923c53ea998f598a7c
Dec 30, 2022 10:59 UTCExplorer

Victim Addresses

0x2b806e6d78d8111dd09c58943b9855910bade005BSC
0x4b02d85e086809eb7af4e791103bc4cde83480d1BSC

Loss Breakdown

1,461.8USDT

Similar Incidents

Root Cause Analysis

DFS Pair Accounting Drain

1. Incident Overview TL;DR

On BSC block 24349822, transaction 0xcddcb447d64c2ce4b3ac5ebaa6d42e26d3ed0ff3831c08923c53ea998f598a7c exploited DFS token 0x2b806e6d78d8111dd09c58943b9855910bade005 and the DFS/USDT PancakeSwap pair 0x4b02d85e086809eb7af4e791103bc4cde83480d1. The attacker EOA 0xb358bfd28b02c5e925b89ad8b0eb35913d2d0805 called helper contract 0x87bfd80c2a05ee98cfe188fd2a0e4d70187db137, borrowed USDT through a flash swap, inflated DFS balances by repeatedly skimming the exempt pair back to itself, swapped forged DFS into USDT, repaid the source pair, and retained 1458131004610788648732 raw USDT units.

The root cause is a broken balance-conservation branch in DFS::_transfer. When a transfer involves the configured pair and exclusiveFromFee[from] is true, DFS still credits to but never decrements from. Because the live DFS/USDT pair had been configured as fee-exempt before the incident, pair-originated transfers and pair self-transfers created spendable DFS balances and made the pair’s USDT reserve publicly drainable.

2. Key Background

DFS is a fee-on-transfer style token with a special branch for transfers that involve a configured pair address. The owner can set that pair with setPair(address) and can mark addresses fee-exempt with setExclusiveFromFee(address[]).

The relevant owner-controlled configuration is visible in the collected DFS source:

function setExclusiveFromFee(address[] memory _exclusive) public onlyOwner{
    for (uint256 i=0;i<_exclusive.length;i++){
        if (!exclusiveFromFee[_exclusive[i]]) {
            exclusiveFromFee[_exclusive[i]] = true;
        }
    }
}
function setPair(address _pair) public onlyOwner {
    pair = IUniswapV2Pair(_pair);
}

PancakeSwap V2 pairs expose skim(to), which transfers any token balance above stored reserves to to. If a token allows the pair to transfer tokens out without reducing the pair’s own balance, skim can repeatedly realize fake surplus. That is exactly what happened here after DFS marked its real DFS/USDT pair as fee-exempt.

The public pre-state immediately before the exploit is fully observable from chain data: DFS already pointed pair() to 0x4b02d85e086809eb7af4e791103bc4cde83480d1, exclusiveFromFee(pair) was true, and the pair held 1462547112307558921470 raw USDT units, which was enough liquidity for a profitable flash-swap drain.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability is a token accounting error, not a private-key compromise or privileged backdoor. DFS intends to treat pair-involved transfers specially, but in the pair branch it only debits _balance[from] when !exclusiveFromFee[from]. If from is the fee-exempt pair itself, the function skips the only sender-debit statement and still credits the recipient. That violates the core invariant that every credited token amount must be matched by an equal sender decrease plus any explicit burn.

The exact breakpoint is in the collected DFS source:

if (to == address(pair) || from == address(pair) ) { 
    if (takeFee && !exclusiveFromFee[from]) {
        fee = amount.mul(rate).div(1000);
        _balance[from] = _balance[from].sub(amount).sub(fee);
        _balance[destroyAddress] = _balance[destroyAddress].add(fee);
        emit Transfer(from, destroyAddress, fee);
    }
} else {
    _balance[from] = _balance[from].sub(amount);
}
...
_balance[to] = _balance[to].add(amount);

Because the configured pair was fee-exempt on-chain, every transfer from the pair credited the receiver with DFS while leaving the pair balance unchanged. A pair self-transfer inside skim(pair) was even worse: the pair credited itself with the skim amount, which compounded the apparent surplus on every iteration. Once the attacker had inflated DFS balances far above the original reserve, swapping forged DFS back into the pair was enough to pull out almost all USDT liquidity.

4. Detailed Root Cause Analysis

The exploit starts with a source flash swap from pair 0x2b948b5d3ebe9f463b29280fc03ebcb82db1072f, which delivered 1462547112307558921470 raw USDT units to the attacker helper. The helper then transferred that borrowed USDT into the DFS/USDT pair and swapped it for 99266250325935225603 raw DFS units. Because the DFS pair itself was fee-exempt, that pair-originated DFS transfer credited the helper without reducing the pair’s DFS balance.

The helper then synchronized the pair and seeded it with 97280925319416521090 raw DFS units before beginning the inflation loop. The seed trace shows the first self-skim cycle clearly:

DFS::transfer(0x4B02...80D1, 97280925319416521090)
emit Transfer(from: 0x4B02...80D1, to: 0x4B02...80D1, value: 97280925319416521090)
storage @ pair balance slot: ...100d6195012727881e -> ...15536cdb2a8447a9a0

That is the accounting break in action. A self-transfer from the pair to itself increased the pair’s recorded DFS balance instead of conserving it. Repeating skim(pair) twelve times doubled the pair-side DFS surplus over and over until a final skim(helper) transferred 398462670108330070384640 raw DFS units to the attacker helper.

After inflating DFS, the helper sent forged DFS back into the DFS/USDT pair and executed the reserve-draining swap. Near the end of the seed trace, the pair paid out 2924343648528141048732 raw USDT units to the helper, after which only 750576086976794208 raw USDT units remained in the pair. The helper then repaid 1466212643917352400000 raw USDT units to the source pair and transferred the residual 1458131004610788648732 raw USDT units to the attacker EOA.

The balance diff confirms the same end state deterministically:

{
  "pair_usdt_delta": "-1461796536220582127262",
  "attacker_usdt_delta": "1458131004610788648732",
  "pair_dfs_delta": "777003630685604562790860"
}

Flash liquidity and the helper contract were only execution aids. The actual root cause was the DFS transfer branch that broke sender debiting for fee-exempt pair senders and allowed a public caller to convert that accounting break into a reserve drain.

5. Adversary Flow Analysis

The adversary cluster contains the initiating EOA 0xb358bfd28b02c5e925b89ad8b0eb35913d2d0805 and helper contract 0x87bfd80c2a05ee98cfe188fd2a0e4d70187db137. The EOA submitted the exploit transaction; the helper executed the entire on-chain sequence.

The execution flow was:

  1. Borrow USDT from source pair 0x2b948b5d3ebe9f463b29280fc03ebcb82db1072f through a flash swap.
  2. Move the borrowed USDT into DFS/USDT pair 0x4b02d85e086809eb7af4e791103bc4cde83480d1 and swap into DFS.
  3. Sync the pair, seed it with DFS, and call skim(pair) twelve times so pair self-transfers repeatedly increase the pair’s apparent DFS balance.
  4. Call skim(helper) to pull the inflated DFS surplus out to the attacker helper.
  5. Transfer forged DFS back into the pair and swap out nearly all USDT reserves.
  6. Repay the original flash swap and transfer the remaining USDT profit to the attacker EOA.

The critical decision point was not hidden attacker state. It was the public knowledge that the live DFS pair was fee-exempt and that pair-originated transfers were therefore unconserved. Any unprivileged user observing the same state could deploy a helper contract and execute the same sequence.

6. Impact & Losses

The measurable loss was borne by the DFS/USDT pair. The collected balance diff shows the pair’s USDT balance fell from 1462547112307558921470 to 750576086976794208, a net loss of 1461796536220582127262 raw USDT units. The attacker EOA ended the transaction with 1458131004610788648732 raw USDT units.

The incident also demonstrates a broader integrity failure in DFS token accounting. The pair’s DFS balance increased by 777003630685604562790860 raw units during the transaction, and both the attacker helper and burn address accumulated large DFS balances that were not backed by a matching sender debit. That means the exploit damaged both AMM reserve integrity and token supply distribution.

7. References

  • Seed exploit transaction: 0xcddcb447d64c2ce4b3ac5ebaa6d42e26d3ed0ff3831c08923c53ea998f598a7c
  • Attacker EOA: 0xb358bfd28b02c5e925b89ad8b0eb35913d2d0805
  • Attacker helper: 0x87bfd80c2a05ee98cfe188fd2a0e4d70187db137
  • DFS token: 0x2b806e6d78d8111dd09c58943b9855910bade005
  • DFS/USDT pair: 0x4b02d85e086809eb7af4e791103bc4cde83480d1
  • Source flash-swap pair: 0x2b948b5d3ebe9f463b29280fc03ebcb82db1072f
  • Evidence used:
    • Collected DFS source code in the seed artifacts.
    • Seed trace for the exploit transaction, including repeated skim(pair) self-transfers and the final reserve-draining swap.
    • Seed balance diff confirming pair depletion and attacker profit.