All incidents

T3913 Pair-Skim Referral Drain

Share
Nov 02, 2023 05:07 UTCAttackLoss: 446,501,628,997.63 3913, 278,798,044,220.11 9419 +1 morePending manual check1 exploit txWindow: Atomic
Estimated Impact
446,501,628,997.63 3913, 278,798,044,220.11 9419 +1 more
Label
Attack
Exploit Tx
1
Addresses
3
Attack Window
Atomic
Nov 02, 2023 05:07 UTC → Nov 02, 2023 05:07 UTC

Exploit Transactions

TX 1BSC
0x8163738d6610ca32f048ee9d30f4aa1ffdb3ca1eddf95c0eba086c3e936199ed
Nov 02, 2023 05:07 UTCExplorer

Victim Addresses

0xd74f28c6e0e2c09881ef2d9445f158833c174775BSC
0x570c19331c1b155c21ccd6c2d8e264785cc6f015BSC
0x715762906489d5d671ea3ec285731975da617583BSC

Loss Breakdown

446,501,628,997.633913
278,798,044,220.119419
31,354.82USDT

Similar Incidents

Root Cause Analysis

T3913 Pair-Skim Referral Drain

1. Incident Overview TL;DR

Transaction 0x8163738d6610ca32f048ee9d30f4aa1ffdb3ca1eddf95c0eba086c3e936199ed on BSC block 33132468 drained T3913 referral inventory by abusing how the token rewards pair-originated transfers. The attacker used public DODO flash liquidity, deployed a fresh helper contract, bound that helper as an invited child, then repeatedly forced PancakeSwap skim() transfers from the public T3913/USDT pair into the helper. Each skim looked like a reward-eligible buy to T3913, so the token paid 6% of the skimmed amount from the invite vault to the attacker-controlled inviter.

The root cause is a logic flaw in T3913, not privileged access. T3913 authenticates invite rewards using only from being a registered pair and _isLiquidity deciding that the transfer is not a liquidity removal. Because _isLiquidity only compares the pair's token0 balance against reserves, the attacker can donate one unit of token0 before each skim and force the transfer to be classified as buy-like. After valuing the sender's BNB gas cost on the public WBNB/USDT route at block 33132467, the attacker still finishes with 31348811381471454645771 raw USDT units of net profit.

2. Key Background

T3913 is an 18-decimal BEP-20 token at 0xd74f28c6e0e2c09881ef2d9445f158833c174775. At deployment it creates two PancakeSwap V2 pairs, one against USDT and one against token 9419, and registers both pairs in _v2Pairs. The contract also deploys an internal referral inventory vault _smartVault_invite, exposed via getMarketing(), from which invite rewards are paid.

Invite binding is not based on signatures or an explicit registration function. Instead, _bindInvite records a relationship after a two-transfer handshake: one account first transfers tokens to another account, and the recipient later transfers back. Once bound, _users[child].pid stores the inviter.

The relevant reward logic is simple:

function _transfer(address from, address to, uint256 amount) internal returns (bool) {
    (bool isAdd, bool isDel) = _isLiquidity(from, to);
    ...
    if (_v2Pairs[from] && !isDel) {
        _inviteBonus(to, amount);
    }
    ...
}

function _inviteBonus(address to, uint256 amount) private {
    if (_users[to].pid != address(0)) {
        uint256 balance_t = _balances[address(_smartVault_invite)];
        if (balance_t == 0) return;
        uint256 bunusAmount = amount.mul(_inviteRate).div(RBASE);
        bunusAmount = bunusAmount > balance_t ? balance_t : bunusAmount;
        _smartVault_invite.transfer(address(this), _users[to].pid, bunusAmount);
    }
}

The liquidity-direction check that gates this path depends only on the pair's token0 side:

function _isLiquidity(address from, address to) private view returns (bool isAdd, bool isDel) {
    ...
    address token0 = IUniswapV2Pair(address(v2Pair)).token0();
    (uint256 r0,,) = IUniswapV2Pair(address(v2Pair)).getReserves();
    uint256 bal0 = IBEP20(token0).balanceOf(address(v2Pair));

    if (token0 != address(this)) {
        if (_v2Pairs[to] && bal0 > r0) isAdd = true;
        if (_v2Pairs[from] && bal0 < r0) isDel = true;
    }
}

For the T3913/USDT pair, token0 is USDT, not T3913. That means the attacker can manipulate the classification of a T3913 transfer by changing only the pair's USDT balance.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability is an application-logic error in how T3913 infers semantic trade events. The contract assumes that any transfer originating from a registered pair is a buy worth rewarding unless _isLiquidity detects a liquidity removal. That assumption is false because PancakeSwap pairs can transfer tokens for housekeeping reasons such as skim(), and those transfers are not authenticated as trades. The _isLiquidity helper is also fragile because it only checks whether the pair's token0 balance is above or below the stored reserve, which an attacker can manipulate with a one-unit donation before the real transfer of T3913. When the pair later sends excess T3913 to an invited helper via skim(), T3913 sees from == pair and isDel == false, so it pays referral rewards from _smartVault_invite even though no real buy occurred. The helper then returns the skimmed T3913 to the orchestrator, letting the attacker recycle the same inventory through the loop. The flaw therefore breaks the invariant that invite-vault payouts should correspond only to genuine purchases of T3913 from a market pair.

4. Detailed Root Cause Analysis

Invite Binding

The trace shows the attacker deploy a fresh helper contract and then perform the two dust transfers needed to set the inviter relationship:

→ new helper @ 0xDE163f9f42123704aeB4E5b4A47180a62DeBC02c
T3913::transfer(0xDE163f..., 1000000000000000000)
0xDE163f...::e76bdea9(T3913, 0x783FBE...)
T3913::transfer(0x783FBE..., 1000000000000000000)

That sequence matches T3913's _bindInvite logic:

function _bindInvite(address to) private {
    address from = msg.sender;
    if (_v2Pairs[from]) return;
    if (_v2Pairs[to]) return;

    if (!_relationInvite[to][from]) {
        if (!_relationInvite[from][to]) {
            _relationInvite[from][to] = true;
        }
    } else {
        if (_users[from].uid == address(0)) {
            _users[from] = User(from, to, block.timestamp);
            _inviters[to].push(_users[from]);
        }
    }
}

After the helper returns the 1 T3913 back to the orchestrator, getUser(helper).pid points to the orchestrator. The revised stakeholder evidence also explains the attacker-side runtimes: the orchestrator stores exploit parameters, creates the helper, and loops the skim sequence, while the helper exposes only one selector that sweeps its entire token balance back to the orchestrator.

Misclassifying Skim Transfers as Buys

Once the attacker has T3913 inventory, the exploit loop is:

  1. Transfer 1 raw USDT to the T3913/USDT pair.
  2. Transfer the attacker's full T3913 balance into that pair.
  3. Call pair.skim(helper).
  4. Let the helper sweep the skimmed T3913 back to the orchestrator.

The critical trace fragment is repetitive and deterministic:

BEP20USDT::transfer(pair_usdt, 1)
T3913::transfer(pair_usdt, 650501978825923088488444996953)
PancakePair::skim(0xDE163f9f42123704aeB4E5b4A47180a62DeBC02c)
0x570C19331c1B155C21ccD6C2D8e264785cc6F015::transfer(T3913, 0x783FBE..., 39030118729555385309306699817)
0xDE163f...::e76bdea9(T3913, 0x783FBE...)

The one-unit USDT donation makes bal0 > r0, so when T3913 later evaluates the outgoing pair transfer during skim(), _isLiquidity leaves isDel == false. The pair is registered in _v2Pairs, the helper is already invite-bound, and _inviteBonus therefore transfers 6% of the skimmed amount from the invite vault to the orchestrator.

This is the exact code-level breakpoint:

if (_v2Pairs[from] && !isDel) {
    _inviteBonus(to, amount);
}

No privileged function is involved. The attacker uses only public pair transfers, skim(), and normal token transfers.

Recycling Capital and Draining the Vault

The helper never holds value for long. Its runtime behavior is just "read my full balance of the provided token and transfer it to the recipient." That makes the attack economically scalable: each skim returns most of the T3913 principal to the attacker while also pulling an extra 6% reward from the invite vault. The trace contains ten consecutive PancakePair::skim calls to the helper, and the seed balance diff shows the invite vault moving from 446501628997631790963039514093 T3913 down to 1000000000000000 wei, effectively emptying it.

Liquidation and Net Profit

After the vault is exhausted, the attacker unwinds the harvested T3913 through the public T3913/USDT and T3913/9419 liquidity routes, then swaps the 9419 proceeds back into USDT and repays the DODO flash-loan pool. The seed balance diff records the sender's direct USDT increase as:

{
  "holder": "0xb29f18b89e56cc0151c7c17de0625a21018d8ae7",
  "before": "10728929264728875630186",
  "after": "42083750547256462722542",
  "delta": "31354821282527587092356"
}

The sender also spent 26243000000000000 wei of BNB gas. A validator-side PancakeRouter getAmountsOut call at block 33132467 prices that amount at 6012869011894978534 raw USDT units on the public WBNB/USDT path. After including the sender's before-and-after BNB portfolio values, the net profit remains 31348811381471454645771 raw USDT units.

5. Adversary Flow Analysis

The full exploit is a single adversary-crafted transaction by EOA 0xb29f18b89e56cc0151c7c17de0625a21018d8ae7, targeting orchestrator contract 0x783fbea45b32eaaa596b44412041dd1208025e83.

First, the orchestrator acquires flash liquidity from public DODO pools. The verbose trace shows nested flashLoan calls culminating in temporary control of 358652959260537946706184 raw USDT units.

Second, the orchestrator deploys helper 0xde163f9f42123704aeb4e5b4a47180a62debc02c, performs the 1-T3913 bind handshake, and swaps enough USDT into T3913 to build recyclable inventory.

Third, the orchestrator runs ten skim cycles against the public T3913/USDT pair. Each cycle donates one unit of USDT, transfers all currently held T3913 into the pair, skims the excess to the helper, receives the 6% invite reward from the vault, and then pulls the skimmed T3913 back from the helper. Because the reward is additive while most principal is preserved, the attacker's T3913 position grows over the loop.

Fourth, the orchestrator exits. It sells T3913 through the T3913/USDT pair, uses the T3913/9419 route for residual inventory, swaps 9419 back to USDT, repays the DODO pools, and leaves the remaining USDT with the original EOA. The trace and balance diff both show that the entire strategy is realized with public calls and no victim-side approvals granted inside the transaction.

6. Impact & Losses

The primary victimized inventory is T3913's invite vault at 0x570c19331c1b155c21ccd6c2d8e264785cc6f015. Its T3913 balance falls by 446501628997630790963039514093 raw units and is left with only 1000000000000000 wei. The attack also externalizes value into surrounding liquidity, reflected by the observed 9419 delta of 278798044220113865039589361217 raw units and a direct USDT gain of 31354821282527587092356 raw units before gas.

Measured at the attacker EOA level and after pricing the recorded BNB gas expenditure on the public WBNB/USDT route, the attacker still realizes 31348811381471454645771 raw USDT units of net positive value. This is therefore a deterministic, profitable ACT attack against T3913's referral accounting.

7. References

  • Incident transaction: 0x8163738d6610ca32f048ee9d30f4aa1ffdb3ca1eddf95c0eba086c3e936199ed on BSC block 33132468.
  • Victim token: T3913 at 0xd74f28c6e0e2c09881ef2d9445f158833c174775.
  • Invite vault: 0x570c19331c1b155c21ccd6c2d8e264785cc6f015.
  • Public pair used for the drain: T3913/USDT pair 0x715762906489d5d671ea3ec285731975da617583.
  • Verified victim source collected in the session: T3913 Contract.sol.
  • Collected seed artifacts: tx metadata, full verbose trace, and balance diff for 0x8163738d6610ca32f048ee9d30f4aa1ffdb3ca1eddf95c0eba086c3e936199ed.
  • Revised stakeholder and profit evidence: validator-reviewed disassembly and fee valuation in stakeholder_profit_evidence.json.