We ran Guardix on BiFi Lending's code from before the $165K drain

October 29, 2026

On October 17, 2026, an attacker drained 165,575 USDT from BiFi Lending on Ethereum. About 159,103.65 USDC came back the next day, so roughly $6.5K was not returned (Olympix post-mortem; the same numbers appear on guardix.io/defi-hacks).

The bug sat in how BiFi updates deposit interest. Deposit interest uses a global exchange rate (EXR). On the first action in a new block, TokenHandler._checkFirstAction multiplies the number of blocks since the market's last action by the current interest rate, then folds that into the EXR. The USDT market had been idle for about 36 days. A flash loan raised utilization, and with it the rate, inside the same transaction. The long idle delta was then multiplied by that high same-block rate.

We pointed Guardix at the public BiFi code as it stood before the attack, at commit ac792dd125 in bifrost-platform/BIFI. The run used the Guardix CLI, completed on 2026-10-29 at 22:12 Europe/Amsterdam, and produced 39 findings: 1 critical, 22 high, 9 medium, 2 low, and 5 info. Guardix flagged _checkFirstAction and the idle-block EXR path.

Bug

The bug

BiFi deposit interest is not accrued continuously on every block. When a market sits idle, the next action pays the whole gap at once.

function _checkFirstAction() internal returns (bool)
{
    // ...
    uint256 blockDelta = sub(currentBlockNumber, lastUpdatedBlock);

    if (blockDelta > 0)
    {
        _handlerDataStorage.setBlocks(currentBlockNumber, blockDelta);
        _handlerDataStorage.syncActionEXR();
        return true;
    }

    return false;
}

_checkFirstAction freezes actionDepositEXR / actionBorrowEXR and inactiveActionDelta at the first action in a block. Later interest math compounds from that frozen snapshot. The jump-rate model still reads live deposit and borrow totals, so a same-block flash loan can raise utilization, and with it the supply interest rate (SIR) and borrow interest rate (BIR), before later actions finish accruing the idle window.

The attacker used that interaction: idle time times a rate that a flash loan could raise inside one transaction.

Reported

What Guardix reported

Guardix named the idle-block / EXR mechanism in several findings. Severities below are Guardix's own labels from this run.

Finding #12 is medium and true positive: "Same-block interest reuses block-open EXR delta while jump rates read post-peer totals." It pointed to _checkFirstAction in contracts/marketHandler/tokenHandler.sol (lines 684-723) and to _calcInterestAmount / _getNewGlobalEXR in contracts/interestModel/interestModel.sol. After the first action in a block, the elapsed-time factor and EXR baseline stay frozen, while SIR/BIR recompute from live totals that later transactions already moved.

Finding #36 is low and a design tradeoff: "Interest apply uses terminal utilization and operator rates over the full idle interval." One SIR/BIR snapshot is compounded across the whole idle block delta since the last EXR sync, so moving utilization right before the next accrual re-prices the dormant period.

Related true positives on the same path:

  • Finding #13 (medium): applyInterest(address) is external with no caller binding. Anyone can force _checkFirstAction / handler-wide syncActionEXR for any user.
  • Finding #2 (high): Unbounded minRate can overflow EXR accrual after a long inactiveActionDelta and brick interest-bearing user actions.

Related high findings Guardix later marked false positive:

  • Finding #26: Stale intra deposit on idle handlers depresses marginCallLimitSum and enables premature liquidation.
  • Finding #27: _getDeltaEXR divides by zero when user EXR is unseeded while principal exists.

Top severity overall is finding #1, critical and true positive: reserveWithdraw can debit depositTotal and send tokens beyond a positive reservedAmount. It lives in tokenHandler.reserveWithdraw (lines 138-148) and handlerDataStorage.subReservedAmount (lines 150-153). subReservedAmount subtracts with signed math and does not require the withdraw to stay within the reserved balance. This is a separate issue from the October 17 drain. Guardix ranked it critical on this commit.

Limits

What this run does not show

  • Guardix rated the idle-block / EXR mechanism medium and low, not critical. The October 17 loss path and Guardix's severity ranking are not the same thing.
  • The selected contract paths for this audit were contracts/marketHandler/tokenHandler.sol and contracts/interestModel/interestModel.sol. Findings outside those paths were not in scope for this run.
  • Two high findings on related EXR/idle paths (#26, #27) are marked false positive in the report. Do not treat every high title as a confirmed exploit.
  • Five findings are design tradeoffs, including #36. That label means Guardix recorded the behavior and did not treat it as a confirmed bug to fix in isolation.
  • This page does not claim Guardix reconstructed the flash-loan transaction that drained the live market. It shows which functions and lines Guardix flagged on the pre-attack commit, and how those map to the public root-cause write-ups.
  • The native PDF report endpoint returned HTTP 500 when this draft was written. Counts and titles come from the completed audit JSON, not from a downloaded PDF.

Your code

Try it on your code

Guardix runs the same audit agents on your contracts. One repository, one commit, $200 flat, results in 2 to 10 hours depending on code size. Public CLI and Claude Code / Cursor plugins: github.com/guardixdev/guardix.

Contact: guardix@dome.net.