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()— selector0x3ccfd60b - File:
blackpanthercontract.sol - Secondary contributing function:
invest(uint256,address,uint256)— selector0xfecba3a5
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
- EOA (
0x6d1cafc8) deploys AttackerContract1 (0xb38cba25), which deploys AttackerContract2 (0xda49ff9b) and callsgo(). - AttackerContract2 approves WBNB to Moolah and flash-loans 270,000 WBNB from Moolah (
0x8f73b65b). - 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). - AttackerContract2 initiates a PancakeSwap V3 USDT/USDC flash (
0x92b7807b) to obtain an additional 34,224,339.05 USDT, bringing total USDT to ~125.9M. - In the PancakeV3 callback (
pancakeV3FlashCallback): a. AttackerContract2 callsinvest(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% (_processUsdtpath: 60% (directBonus[AC2] += 6,200,709.22 USDTis recorded instantly. d. AttackerContract2 immediately callswithdraw()on InfinitySix. The call attemptsupdateTwap(), which exits early because the sameblock.timestampwas already recorded 0 seconds ago.twapPriceremains at ~1.05 USDT/i6 (the pre-attack value). The contract convertsdirectBonus + 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). - 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.
- 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 fromfunds_flow.jsonattacker_gains). - InfinitySix staking contract net i6 loss:
funds_flow.jsonrecords a net change of −5,473,038.40 i6 at address0x1cb36b0f. 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
| Function | Signature | Computed Selector | Trace Selector |
|---|---|---|---|
invest(uint256,address,uint256) | InfinitySix | 0xfecba3a5 | 0xfecba3a5 ✓ |
withdraw() | InfinitySix | 0x3ccfd60b | 0x3ccfd60b ✓ |
flashLoan(address,uint256,bytes) | Moolah | 0xe0232b42 | 0xe0232b42 ✓ |
onMoolahFlashLoan(uint256,bytes) | AC2 callback | 0x13a1a562 | 0x13a1a562 ✓ |
pancakeV3FlashCallback(uint256,uint256,bytes) | AC2 callback | 0xa1d48336 | 0xa1d48336 ✓ |
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.