Crypto exploits

BiFi exploit analysis

The idle BiFi USDT market stored a 264,059-block gap on the first action in a block and then charged that full gap on every later action in the same block, at a utilisation rate the attacker set with permissionless deposits and borrows. DeFiHackLabs records about 165,574 USDT drained on Ethereum.

David

Security ·

Reported loss
about 165,574 USDT drained from the BiFi USDT market on Ethereum, per DeFiHackLabs; the attacker contract held 165,574.514633 USDT of surplus, swapped to 68.057203111718789216 WETH
Occurred
Logged

Logged as “2026-10-17: The Idle Bifi Usdt Market Freezes Its Per Block Exchange Rate And Stores The Entire 264059 Block Inactive Gap On The First Action Then The Interest Model Charges That Full 36 Day Gap Of Deposit Interest On Every Later Action In The Same Block At A Utilization Rate The Attacker Sets Through Permissionless Deposit And Borrow So The Attacker Flash Borrows 405M Usdt From Morpho Deposits It To Seed The Frozen Gap Borrows To Spike Utilization Runs A Repay And Withdraw Loop Twice To Cash Out The Inflated Deposit Interest Then Repays The Loan And Swaps The 165574 Usdt Surplus To 680572 Weth on BiFi”.

Analysis3 min read

The BiFi USDT market on Ethereum had been left alone for 264,059 blocks, roughly 36 days. In the block where that gap was finally cashed, a single transaction withdrew 165,574.514633 USDT from the market and converted it. The market charged the full gap's interest again on every action in that block, at a rate the payer chose. DeFiHackLabs records the incident and ships a proof of concept that replays it on a fork of Ethereum mainnet.

Idle time, stored once

BiFi accrues lazily rather than per block. Nothing moves when the market is empty, so the exchange rate is only settled when someone acts. The first action in a block, detected in TokenHandler as _checkFirstAction, does two things: it freezes the per-block rate for that block, and it stores the entire gap since the last action as the inactive block delta.

Both are visible in the verified interest model and verified handler, both compiled with solc 0.6.12. _getInterestAmount reads that stored delta and the frozen rate and passes both straight through, and _getNewGlobalEXR multiplies the rate by the delta in full. The delta is never split across the blocks it covers and never clamped to one block.

That is fine for one action. The stored gap survives every later action in the same block, because the condition that writes it only holds for the first one. So the second action in that block is charged the whole gap's interest, and so is the third, and the twentieth.

The rate was the attacker's to set

The frozen rate decides how much interest, but it is not the only input. _calcInterestAmount derives the supply and borrow rates from the market's current deposit and borrow totals through _getUtilizationRate, and those totals are whatever depositors and borrowers have outstanding at that moment. Both sides of that ratio are permissionless.

An attacker who deposits to lift the denominator and then borrows to push the ratio where they want it is choosing the rate at which the stored delta is about to be applied.

Nothing gates any of it. The proxy exposes deposit, withdraw, borrow and repay directly, each a plain delegatecall into the handler with no owner, keeper or role check, and the idle state is elapsed time rather than a privileged input.

The sequence

  1. Flash-borrow 40,499,999.999997 USDT from Morpho Blue at zero fee.
  2. Deposit it into the USDT market in five growing tranches. This is the block's first action, so it freezes the rate and stores the idle gap.
  3. Borrow 1,498,500 USDT twenty times, driving utilisation up so the live rate is the attacker's.
  4. Run a repay and withdraw loop twenty times, pulling the inflated deposit interest back out.
  5. Repeat the deposit, borrow and loop once more, then withdraw the residual.
  6. Repay the flash loan and swap the surplus.

The proof of concept reads lastUpdatedBlock on the fork as 25,946,196. Measured to block 26,210,255, where the exploit contract was deployed, that is the 264,059-block gap the entry describes. The profit transaction landed one block later, at 26,210,256, and the proof of concept asserts a 264,060-block gap at that block.

Who took the money

Two addresses appear in this incident and they are not the same party. The original exploiter, 0x7c78a049fff71609990017b58a959e926e6ec4c6, deployed its exploit contract in block 26,210,255. One block later, the MEV bot c0ffeebabe (0xc0ffeebabe5d496b2dde509f9fa189c25cf29671) took the profit through its own searcher contract, 0xe08d97e151473a848c3d9ca3f323cb720472d015. DeFiHackLabs records that the bot front-ran the same permissionless mechanism rather than using the exploiter's contract. The profit transaction ends with the searcher contract holding 165,574.514633 USDT of surplus, swapped to 68.057203111718789216 WETH.

Where the guard was missing

A delta spanning 36 days needs to be consumed once, or at least amortised against the blocks that actually elapsed between actions. Storing it once and charging it on every action in a block turns one period of inactivity into an arbitrarily large number of periods. Charging the rate from live totals rather than from the totals as they stood when the gap opened is what lets the payer pick the multiplier.

Sources read (5)

Other exploits

Query every incident yourself — free, no signup

Updated 11 Nov 2026. Figures are as reported by the sources listed above and are not independently verified.