InfinitySix Stale TWAP Price Exploitation (BSC)

Two compounding flaws in InfinitySix’s ($i6) BSC staking contract were chained to extract 273,802 USDT in block 89,703,286. The contract credits referral bonuses to a sponsor’s withdrawable balance immediately upon the referral’s invest() call; separately, its TWAP oracle enforces a 1-minute hard refresh floor, making same-block price updates structurally impossible. The attacker flash-borrowed ~$125.9M USDT, routed $124M through a disposable helper contract as a self-referral to manufacture $6.2M in instant withdrawable bonus, then called withdraw() before the TWAP could advance. The contract settled the payout at the pre-attack rate (~1.05 USDT/i6) despite the LP spot having been driven to ~15,528 USDT/i6, issuing ~5.6M i6 tokens instead of the ~399 tokens the current price would have permitted. Those tokens were sold back into the LP for ~$125.2M USDT, all loans were repaid, and the attacker pocketed the difference.

Root Cause

Vulnerable Contract

  • Name: InfinitySix (staking/reward contract)
  • Address: 0x1cb36b0f1efd9b738997da3d5525364c7e82a18a
  • Proxy: No (direct deployment)
  • Source type: Verified (blackpanthercontract.sol)

Vulnerable Function

  • Function: withdraw() — selector 0x3ccfd60b
  • File: blackpanthercontract.sol
  • Secondary contributing function: invest(uint256,address,uint256) — selector 0xfecba3a5

Vulnerable Code

// blackpanthercontract.sol — withdraw()
function withdraw() external nonReentrant {
    if(uniswapPair != address(0)) {
         updateTwap();   // <-- TWAP UPDATE ATTEMPTED HERE
    }

    _updateCompounding(msg.sender);
    User storage user = users[msg.sender];
    require(user.totalDeposits > 0 && !user.isCapped, "No active investment or 7x capped");

    // ... (RWP, level rewards, salary, upline income accumulation) ...

    uint256 totalUsdtToWithdraw = totalAvailableRwpToWithdraw + user.directBonus /* <-- VULNERABILITY: directBonus is the exploited field */
        + user.levelRewardsRealized + user.pendingUplineIncome + user.unwithdrawnSalary;
    require(totalUsdtToWithdraw > 0, "Nothing to withdraw");

    // ...

    uint256 effectivePrice = twapPrice > minTwapPrice ? twapPrice : minTwapPrice; // <-- VULNERABILITY: stale TWAP used here

    uint256 tokensToTransfer = (totalUsdtToWithdraw * WAD) / effectivePrice; // <-- VULNERABILITY: division by stale price inflates token amount
    require(projectToken.balanceOf(address(this)) >= tokensToTransfer, "Not enough tokens in contract");

    uint256 burnAmount = (tokensToTransfer * 5) / 100;
    uint256 userAmount = tokensToTransfer - burnAmount;
    // ...
    require(projectToken.transfer(msg.sender, userAmount), "Token transfer failed");

The updateTwap() function enforces a 1-minute minimum update interval:

// blackpanthercontract.sol — updateTwap()
uint32 public constant TWAP_UPDATE_INTERVAL = 1 minutes; // <-- VULNERABILITY: prevents same-tx update

function updateTwap() public {
    // ...
    uint8 lastIndex = observationIndex == 0 ? 2 : observationIndex - 1;
    Observation memory lastObs = observations[lastIndex];

    if (lastObs.timestamp != 0) {
        uint32 timeSinceLastUpdate;
        unchecked { timeSinceLastUpdate = currentTimestamp - lastObs.timestamp; }
        if (timeSinceLastUpdate < TWAP_UPDATE_INTERVAL) return; // <-- EARLY RETURN: TWAP not updated within 1 minute
    }
    // ... (compute new TWAP from cumulative price) ...
}

The invest() function accrues the referral bonus instantly with no delay:

// blackpanthercontract.sol — invest()
if (user.referrer != address(0)) {
    User storage refUser = users[user.referrer];
    if (!refUser.isCapped) {
        refUser.directBonus += (usdtAmount * DIRECT_BONUS_RATE) / 1000; // <-- VULNERABILITY: instant 5% bonus, no cooldown
    }
    // ...
}

Why It’s Vulnerable

Expected behavior: The withdraw() function should convert accumulated USDT rewards to i6 tokens at a price that reflects current market conditions. If the contract uses a TWAP to prevent price manipulation, the TWAP window should be short enough that it cannot be exploited within a single transaction, OR same-block or same-transaction withdrawals should be blocked after a large invest.

Actual behavior (flaw 1 — stale TWAP): The TWAP_UPDATE_INTERVAL is 1 minute. Because blockchain timestamps don’t advance within a single transaction, calling updateTwap() inside withdraw() will always hit the timeSinceLastUpdate < TWAP_UPDATE_INTERVAL guard and return early if the last observation was recorded less than 60 seconds before the current block. The attacker’s invest() call, which also calls updateTwap(), and the subsequent withdraw() in the same transaction share the same block.timestamp. As a result, withdraw() uses the TWAP price recorded in a previous block (1.05 USDT/i6), ignoring the catastrophic LP reserve distortion ($124M USDT added, spot price → ~15,528 USDT/i6) caused by the massive referral invest.

Actual behavior (flaw 2 — instant directBonus): When a user invests with a referrer, 5% of the investment amount is immediately credited to users[referrer].directBonus. There is no delay, no cap relative to referrer’s own stake, and no withdrawal cooldown. An attacker can thus flash-loan any amount, invest it through a helper contract citing themselves as referrer, and immediately claim the entire 5% bonus in the same transaction via withdraw().

Combined effect: The attacker’s directBonus after the ~$124M referral invest is:

directBonus = 124,014,184.40 USDT × 50 / 1000 = 6,200,709.22 USDT

At the stale TWAP price (~1.05 USDT/i6), withdraw() converts this as:

tokensToTransfer = 6,200,709.22 × 1e18 / (1.05 × 1e18) ≈ 5,905,437 i6

At fair post-manipulation spot price (~15,528 USDT/i6), the fair token output would be:

tokensToTransfer_fair = 6,200,709.22 × 1e18 / (15,528 × 1e18) ≈ 399 i6

The ~14,800× price discrepancy (stale vs. spot) is what enables the outsized profit.

Attack Execution

High-Level Flow

  1. EOA (0x6d1cafc8) deploys AttackerContract1 (0xb38cba25), which deploys AttackerContract2 (0xda49ff9b) and calls go().
  2. AttackerContract2 approves WBNB to Moolah and flash-loans 270,000 WBNB from Moolah (0x8f73b65b).
  3. In the Moolah callback (onMoolahFlashLoan): WBNB is unwrapped to BNB, deposited into Venus vBNB (0xa07c5b74) as collateral, and 91,675,660.95 USDT is borrowed from Venus vUSDT (0xfd5840cd).
  4. AttackerContract2 initiates a PancakeSwap V3 USDT/USDC flash (0x92b7807b) to obtain an additional 34,224,339.05 USDT, bringing total USDT to ~125.9M.
  5. In the PancakeV3 callback (pancakeV3FlashCallback): a. AttackerContract2 calls invest(885,815.60 USDT, GENESIS_USER, 0) on InfinitySix, registering itself as a valid user under the genesis sponsor and recording the first TWAP observation. The invest also swaps/adds LP liquidity, setting an observation at the current pre-attack price. b. AttackerContract2 deploys AttackerContract3 (0x096ab739) and sends it 124,014,184.40 USDT. c. AttackerContract3 calls invest(124,014,184.40 USDT, AttackerContract2, 0) on InfinitySix — a $124M referral invest naming AttackerContract2 as sponsor. This triggers the _processUsdt path: 60% ($74.4M) is swapped USDT→i6, 40% ($49.6M) adds LP liquidity. Combined, this floods the LP with USDT, driving the spot i6 price up ~14,000×. Simultaneously, directBonus[AC2] += 6,200,709.22 USDT is recorded instantly. d. AttackerContract2 immediately calls withdraw() on InfinitySix. The call attempts updateTwap(), which exits early because the same block.timestamp was already recorded 0 seconds ago. twapPrice remains at ~1.05 USDT/i6 (the pre-attack value). The contract converts directBonus + totalAvailableRwpToWithdraw (~6.2M USDT) at the stale price, transferring 5,601,682.60 i6 to AttackerContract2. e. AttackerContract2 approves and swaps 5,545,665.77 i6 back into the PancakeV2 LP for 125,177,224.44 USDT (net of AMM slippage at the inflated reserve ratio).
  6. AttackerContract2 repays the PancakeV3 flash loan (~34,227,761.48 USDT) and the Venus vUSDT loan (~91,675,660.95 USDT), then redeems vBNB collateral, wraps BNB back to WBNB, and repays the Moolah 270,000 WBNB flash loan.
  7. Remaining 273,802.005417 USDT is transferred to AttackerContract1, then to EOA 0x6d1cafc8.

Detailed Call Trace

EOA 0x6d1cafc8
  CREATE → AttackerContract1 (0xb38cba25)
    CREATE → AttackerContract2 (0xda49ff9b)
      CREATE → AttackerContract3 (0x096ab739)
    CALL  0xda49ff9b :: go() [0x0f59f83a]
      CALL  WBNB :: approve(Moolah, max) [0x095ea7b3]
      CALL  Moolah 0x8f73b65b :: flashLoan(WBNB, 270000e18, ...) [0xe0232b42]
        DELEGATECALL → MoolahImpl 0x9321587e :: flashLoan(...)
          CALL  WBNB :: transfer(0xda49ff9b, 270000e18) [0xa9059cbb]
          CALL  0xda49ff9b :: onMoolahFlashLoan(270000e18, ...) [0x13a1a562]
            CALL  WBNB :: withdraw(270000e18) [0x2e1a7d4d]  ← unwrap BNB
            CALL  VenusVBNB 0xa07c5b74 :: mint() [0x1249c58b] {value: 270000 BNB}  ← mint vBNB collateral
              CALL  VenusComptroller :: mintAllowed(...)
              CALL  VenusComptroller :: mintVerify(...)
            CALL  VenusVUSDT 0xfd5840cd :: borrow(91,675,660.95e18 USDT) [0xc5ebeaec]
              CALL  VenusComptroller :: borrowAllowed(...)  ← price oracle consulted (I6 TWAP used for vI6 collateral value)
            CALL  PancakeV3 0x92b7807b :: flash(0xda49ff9b, 34,224,339.05e18, 0, ...) [0x490e6cbc]
              CALL  USDT :: transfer(0xda49ff9b, 34,224,339.05e18) [0xa9059cbb]
              CALL  0xda49ff9b :: pancakeV3FlashCallback(34,224,339.05e18, 0, ...) [0xa1d48336]
                ── [Attacker invest, small] ──
                CALL  USDT :: approve(InfinitySix, 885815.60e18) [0x095ea7b3]
                CALL  InfinitySix 0x1cb36b0f :: invest(885815.60e18, GENESIS_USER, 0) [0xfecba3a5]
                  CALL  USDT :: transferFrom(0xda49ff9b, InfinitySix, 885815.60e18) [0x23b872dd]
                  CALL  PancakeV2Pair :: getReserves() [0x0902f1ac]
                  CALL  PancakeV2Router :: swapExactTokensForTokens(531489.36 USDT → i6) [0x38ed1739]
                    CALL  PancakeV2Pair :: swap(...) [0x022c0d9f]  ← i6 out to InfinitySix
                  CALL  PancakeV2Router :: addLiquidity(354326.24 USDT + i6) [0xe8e33700]
                  [updateTwap() records observation at pre-attack cumulative price]
                ── [Referral invest, massive] ──
                CALL  USDT :: transfer(0x096ab739, 124,014,184.40e18) [0xa9059cbb]
                CALL  0x096ab739 :: referralInvest(USDT, InfinitySix, 0xda49ff9b, 124014184.40e18) [0x6506baaf]
                  CALL  USDT :: approve(InfinitySix, 124014184.40e18) [0x095ea7b3]
                  CALL  InfinitySix 0x1cb36b0f :: invest(124014184.40e18, 0xda49ff9b, 0) [0xfecba3a5]
                    CALL  USDT :: transferFrom(0x096ab739, InfinitySix, 124014184.40e18) [0x23b872dd]
                    ← directBonus[0xda49ff9b] += 6,200,709.22 USDT  (state write, instant)
                    CALL  PancakeV2Router :: swapExactTokensForTokens(74408510.64 USDT → i6) [0x38ed1739]
                    CALL  PancakeV2Router :: addLiquidity(49605673.76 USDT + i6) [0xe8e33700]
                    [updateTwap() attempts update — same timestamp, exits early → twapPrice UNCHANGED]
                ── [Withdraw at stale TWAP] ──
                CALL  InfinitySix 0x1cb36b0f :: withdraw() [0x3ccfd60b]
                  [updateTwap() attempts update — same block.timestamp → exits early → twapPrice still ~1.05]
                  ← totalUsdtToWithdraw ≈ 6,200,709.22 USDT (directBonus + small RWP)
                  ← tokensToTransfer = 6,200,709.22e18 / 1.05e18 ≈ 5,905,437 i6
                  CALL  i6Token 0xd7684971 :: transfer(0xda49ff9b, 5,601,682.60e18) [0xa9059cbb]
                  CALL  i6Token 0xd7684971 :: burn(294,825.40e18) [0x42966c68]  ← 5% burn
                ── [Dump i6 → USDT] ──
                CALL  i6Token :: approve(PancakeV2Router, 5,545,665.77e18) [0x095ea7b3]
                CALL  PancakeV2Router :: swapExactTokensForTokens(5,545,665.77 i6 → 125,177,224.44 USDT) [0x5c11d795]
                  CALL  PancakeV2Pair :: swap(...) [0x022c0d9f]  ← USDT out to 0xda49ff9b
              ── [Repay PancakeV3 flash] ──
              CALL  USDT :: transfer(PancakeV3, 34,227,761.48e18) [0xa9059cbb]
            ── [Repay Venus borrow] ──
            CALL  USDT :: approve(vUSDT, max) [0x095ea7b3]
            CALL  VenusVUSDT :: repayBorrow(91,675,660.95e18) [0x0e752702]
            CALL  VenusVBNB :: redeemUnderlying(270000e18 BNB) [0x852a12e3]
            CALL  WBNB :: deposit() {value: 270000 BNB} [0xd0e30db0]  ← rewrap BNB
          ── [Repay Moolah flash] ──
          CALL  WBNB :: transfer(Moolah, 270000e18) [0xa9059cbb]
      ── [Profit extraction] ──
      CALL  USDT :: transfer(0xb38cba25, 273802.005417e18) [0xa9059cbb]
  CALL  USDT :: transfer(EOA 0x6d1cafc8, 273802.005417e18) [0xa9059cbb]

Financial Impact

  • Attacker profit (primary source): 273,802.005417 USDT transferred to attacker EOA 0x6d1cafc890cc7dd6bf3718453367f8e0fd9851e4 (confirmed from funds_flow.json attacker_gains).
  • InfinitySix staking contract net i6 loss: funds_flow.json records a net change of −5,473,038.40 i6 at address 0x1cb36b0f. These i6 tokens were drawn from the contract’s project-token reserve, which exists to pay out future user rewards. The protocol’s ability to honor future reward withdrawals is severely degraded.
  • PancakeV2 LP net: −277,224.44 USDT (USDT drained from LP reserves after the massive add+dump cycle). LP token holders absorbed this slippage loss.
  • Flash loan costs: The PancakeSwap V3 flash fee (0.01% × $34.2M) = ~$3,422 USDT was retained by the pool. Moolah charges no fee on single-block flash loans. Venus borrow interest for a single block is negligible.
  • Protocol solvency: The InfinitySix staking contract’s i6 reserve is critically depleted. Users with active investments may be unable to withdraw at the contracted reward rate.

Evidence

Key Transaction Facts

  • Tx hash: 0xc1b9a237a00b53a595e1e2d0d93841154ddcdf9aa217be8f395449b8e4ab2f16
  • Block: 89,703,286 (BNB Smart Chain)
  • Attacker EOA: 0x6d1cafc890cc7dd6bf3718453367f8e0fd9851e4
  • Receipt status: 0x1 (success)

Selector Verification

FunctionSignatureComputed SelectorTrace Selector
invest(uint256,address,uint256)InfinitySix0xfecba3a50xfecba3a5
withdraw()InfinitySix0x3ccfd60b0x3ccfd60b
flashLoan(address,uint256,bytes)Moolah0xe0232b420xe0232b42
onMoolahFlashLoan(uint256,bytes)AC2 callback0x13a1a5620x13a1a562
pancakeV3FlashCallback(uint256,uint256,bytes)AC2 callback0xa1d483360xa1d48336

Token Transfer Evidence (from funds_flow.json)

  • Transfer index 13: 0xda49ff9b → 0x096ab739: 124,014,184.397 USDT (referral helper funded)
  • Transfer index 14: 0x096ab739 → InfinitySix 0x1cb36b0f: 124,014,184.397 USDT (referral invest)
  • Transfer index 23: InfinitySix → 0xda49ff9b: 5,601,682.600622 i6 (stale-price withdraw payout)
  • Transfer index 27: PancakeV2Pair → 0xda49ff9b: 125,177,224.439 USDT (i6 dump proceeds)
  • Transfer index 32: 0xda49ff9b → 0xb38cba25: 273,802.005417 USDT (profit)
  • Transfer index 33: 0xb38cba25 → EOA: 273,802.005417 USDT (final exfiltration)

InfinitySix Storage Post-State (from trace_prestateTracer.json)

The observations array and twapPrice slot at 0x1cb36b0f confirm the TWAP state was updated during invest() but not during withdraw() (same block.timestamp guard triggered the early return). The directBonus storage slot for 0xda49ff9b in the users mapping reflects the full ~6.2M USDT bonus written during the referral invest() call.