# 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 it124,014,184.40 USDT. c. AttackerContract3 calls$49.6M) adds LP liquidity. Combined, this floods the LP with USDT, driving the spot i6 price up ~14,000×. Simultaneously, `invest(124,014,184.40 USDT, AttackerContract2, 0)` on InfinitySix — a$124M referral invest naming AttackerContract2 as sponsor. This triggers the$74.4M) is swapped USDT→i6, 40% ( `_processUsdt` path: 60% ( `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 SelectorInfinitySixInfinitySixMoolahAC2 callbackAC2 callback
### 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.
