All incidents

VTF Virtual Balance Drain

Share
Oct 25, 2022 04:20 UTCAttackLoss: 59,060.63 USDTPending manual check2 exploit txWindow: 2d 5h
Estimated Impact
59,060.63 USDT
Label
Attack
Exploit Tx
2
Addresses
2
Attack Window
2d 5h
Oct 25, 2022 04:20 UTC → Oct 27, 2022 09:40 UTC

Exploit Transactions

TX 1BSC
0xc2d2d7164a9d3cfce1e1dac7dc328b350c693feb0a492a6989ceca7104eef9b7
Oct 25, 2022 04:20 UTCExplorer
TX 2BSC
0xeeaf7e9662a7488ea724223c5156e209b630cdc21c961b85868fe45b64d9b086
Oct 27, 2022 09:40 UTCExplorer

Victim Addresses

0xc6548caf18e20f88cc437a52b6d388b0d54d830dBSC
0x825ef6a0f26a06367d6415fd34f9c9174fdf11e1BSC

Loss Breakdown

59,060.63USDT

Similar Incidents

Root Cause Analysis

VTF Virtual Balance Drain

1. Incident Overview TL;DR

The incident is a permissionless drain of the live VTF/USDT pair on BNB Smart Chain. The attacker first deployed an exploit contract in tx 0xc2d2d7164a9d3cfce1e1dac7dc328b350c693feb0a492a6989ceca7104eef9b7, then later executed the profit-taking path in tx 0xeeaf7e9662a7488ea724223c5156e209b630cdc21c961b85868fe45b64d9b086. The core issue is that VTF reports unminted time-accrual rewards inside balanceOf, then lets any caller materialize those rewards with updateUserBalance(address) and ordinary transfers. Because the VTF/USDT pair trusts token balanceOf as canonical inventory, the attacker could compound VTF across aged helper contracts and dump the amplified balance into the pool for 58450654966349820134604 wei-USDT.

2. Key Background

VTF is a reward-bearing token at 0xc6548caf18e20f88cc437a52b6d388b0d54d830d. Any address with at least 100 VTF can accrue a time-based reward of roughly 1% of stored balance per day after userBalanceTime[address] is initialized. The relevant problem is architectural: pending rewards are not tracked in a separate claim ledger. Instead, VTF exposes them directly through balanceOf, which makes external integrations observe inventory that has not yet been minted into ERC20 storage.

The paired venue is the VTF/USDT P2EPair at 0x825ef6a0f26a06367d6415fd34f9c9174fdf11e1. Its reserve accounting path uses IERC20(token).balanceOf(address(this)) in mint, burn, swap, skim, and sync. That assumption is only safe if the token's balanceOf reflects actual transferable units.

3. Vulnerability Analysis & Root Cause Summary

The bug is a balance-accounting violation in VTF, not a pricing-only issue. VTF overrides balanceOf(address) to return stored balance plus pending mintable rewards, so read-time balances exceed principal already minted into storage. It then exposes updateUserBalance(address) as a public function, allowing anyone to initialize timers on arbitrary addresses and later realize accrued rewards for those addresses. The transfer path makes the issue worse because _transfer calls updateUserBalance on sender and receiver before moving tokens, which lets a caller turn the virtual amount reported by balanceOf into transferable balance during the same transfer sequence. The attacker used that property to pre-age helper contracts, move VTF through them one by one, and compound the position. P2EPair then accepted the inflated VTF amount as genuine input inventory and paid out USDT against it.

4. Detailed Root Cause Analysis

The decisive VTF logic is below:

mapping(address => uint256) private userBalanceTime;
function balanceOf(address account) public override view returns (uint256) {
    return super.balanceOf(account).add(getUserCanMint(account));
}

function getUserCanMint(address account) public view returns (uint256) {
    uint256 userStartTime = userBalanceTime[account];
    uint256 haveAmount = super.balanceOf(account);
    if (userStartTime > 0 && haveAmount >= 10**20 && tokenStartTime < block.timestamp && maxCanMint && managerCanMint) {
        uint256 secondAmount = haveAmount.div(100).div(86400);
        uint256 afterSecond = block.timestamp.sub(userStartTime);
        return secondAmount.mul(afterSecond);
    }
    return 0;
}

function updateUserBalance(address _user) public {
    if (userBalanceTime[_user] > 0) {
        uint256 canMint = getUserCanMint(_user);
        if (canMint > 0) {
            userBalanceTime[_user] = block.timestamp;
            _mint(_user, canMint);
        }
    } else {
        userBalanceTime[_user] = block.timestamp;
    }
}

This violates the core ERC20-style invariant that externally observed balance should match the amount an account can transfer without a separate mint side effect. The transfer path realizes the break:

if (from == uniswapV2Pair) {
    updateUserBalance(to);
} else if (to == uniswapV2Pair) {
    updateUserBalance(from);
} else {
    updateUserBalance(from);
    if (to != _destroyAddress) {
        updateUserBalance(to);
    }
}

That means a helper contract can hold VTF long enough to accrue a positive getUserCanMint, then transfer balanceOf(helper) and have the pending amount minted immediately before the transfer executes. The exploit path is therefore:

  1. Deploy attacker infrastructure and initialize many helper contracts by calling public updateUserBalance on them.
  2. Wait for time to pass so each helper's timer ages.
  3. Buy a seed VTF position with USDT.
  4. Send the VTF balance through the helper chain. At each hop, the current helper realizes newly accrued rewards into storage and forwards the larger amount.
  5. Sell the amplified VTF back to the live pair for USDT.

P2EPair amplifies the impact because it trusts token balances directly:

uint balance0 = IERC20(token0).balanceOf(address(this));
uint balance1 = IERC20(token1).balanceOf(address(this));
...
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
...
_update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);

The pair cannot distinguish VTF's synthetic read-time inflation from real reserves, so it pays USDT against the inflated VTF input.

5. Adversary Flow Analysis

The attacker EOA 0x57c112cf4f1e4e381158735b12aaf8384b60e1ce deployed exploit contract 0x450595e4a42cc08c14091b08dbab654a68b0a877 in block 22471982. The deployment metadata shows a contract-creation transaction whose bytecode embeds VTF, pair, USDT, router, and helper addresses.

The profit-taking transaction happened in block 22535103. The trace shows the exploit contract buying VTF through the public router, repeatedly realizing rewards, and then exiting back to USDT. Representative trace evidence:

swapExactTokensForTokensSupportingFeeOnTransferTokens(
  99999999999999991601392, 0, [USDT, VTF], attacker_contract, 1666863608
)
...
VTF::updateUserBalance(...)
VTF::transfer(..., 499803157482531199370634948)
...
swapExactTokensForTokensSupportingFeeOnTransferTokens(
  499803157482531199370634948, 0, [VTF, USDT], attacker_contract, 1666863608
)
...
BEP20USDT::transfer(attacker_eoa, 58450654966349820134604)

The trace also shows a long series of VTF::updateUserBalance calls across attacker-controlled helper addresses before the final sale, which is consistent with the compounding model in the validated root cause.

6. Impact & Losses

The measurable victim loss is on the VTF/USDT pair. The balance diff for tx 0xeeaf7e9662a7488ea724223c5156e209b630cdc21c961b85868fe45b64d9b086 shows:

{
  "pair_usdt_delta": "-59060634299711755079085",
  "attacker_eoa_usdt_delta": "58450654966349820134604",
  "vtf_contract_usdt_delta": "609979333361934944481"
}

So the pair lost 59060634299711755079085 wei-USDT in total. The attacker EOA directly realized 58450654966349820134604 wei-USDT, while 609979333361934944481 wei-USDT remained inside the VTF token contract through the token's fee path. Gas was negligible relative to the extracted USDT.

7. References

  • Exploit deployment tx: 0xc2d2d7164a9d3cfce1e1dac7dc328b350c693feb0a492a6989ceca7104eef9b7
  • Profit-taking tx: 0xeeaf7e9662a7488ea724223c5156e209b630cdc21c961b85868fe45b64d9b086
  • Attacker EOA: 0x57c112cf4f1e4e381158735b12aaf8384b60e1ce
  • Exploit contract: 0x450595e4a42cc08c14091b08dbab654a68b0a877
  • VTF token: 0xc6548caf18e20f88cc437a52b6d388b0d54d830d
  • VTF/USDT pair: 0x825ef6a0f26a06367d6415fd34f9c9174fdf11e1
  • Verified VTF source: /workspace/session/artifacts/collector/seed/56/0xc6548caf18e20f88cc437a52b6d388b0d54d830d/src/Contract.sol
  • Verified P2EPair source: /workspace/session/artifacts/collector/seed/56/0x825ef6a0f26a06367d6415fd34f9c9174fdf11e1/src/swap/P2EPair.sol
  • Exploit trace: /workspace/session/artifacts/collector/seed/56/0xeeaf7e9662a7488ea724223c5156e209b630cdc21c961b85868fe45b64d9b086/trace.cast.log
  • Exploit balance diff: /workspace/session/artifacts/collector/seed/56/0xeeaf7e9662a7488ea724223c5156e209b630cdc21c961b85868fe45b64d9b086/balance_diff.json