Dev.to Security 🔐 Cybersecurity 👁 0 📖 3 min read

Dust in the denominator: a guard for interest-index accrual (after the Kava Lend exploit)

On 14 November 2026 an attacker drained about 55.2 million KAVA (about $3.76 million at the pre-attack price) from Kava Lend. According to The Block's on-chain analysis, the way in was a nearly idle BUSD market. A small

On 14 November 2026 an attacker drained about 55.2 million KAVA (about $3.76 million at the pre-attack price) from Kava Lend. According to The Block's on-chain analysis, the way in was a nearly idle BUSD market. A small loan cut that market's net supply to two hundred-millionths of a dollar. The interest calculation divides by net supply, and the code skipped a supply of zero but not a tiny positive one. Over the next three minutes deposit values in the market rose about 243,000 times, and the attacker borrowed KAVA 127 times against the inflated collateral.

The failure class is old and easy to reproduce: a ratio whose denominator an outside party can push toward zero. Below is a toy model of it and two guards. It is not Kava's code; it only models the pattern the reports describe.

The model

// index-guard.mjs
// A toy lending market: one interest index per market, updated by dividing
// the interest owed by the market's net supply. Not Kava's code; a model of
// the failure class described in reports on the 14 Nov 2026 Kava Lend exploit.
// usage: node index-guard.mjs

function accrue(market, interest, { guard = false } = {}) {
  const net = market.supplied - market.borrowed;
  if (net === 0) return { ...market, skipped: 'zero net supply' };
  if (guard) {
    if (net < market.minNetSupply) return { ...market, skipped: `net supply ${net} below floor ${market.minNetSupply}` };
  }
  const growth = 1 + interest / net;
  if (guard && growth > market.maxGrowthPerStep) {
    return { ...market, skipped: `index growth x${growth.toFixed(2)} above cap x${market.maxGrowthPerStep}` };
  }
  return { ...market, index: market.index * growth, skipped: null };
}

const base = {
  supplied: 1000.00000002, // a nearly idle market
  borrowed: 1000,          // one loan sized to leave almost nothing
  index: 1,
  minNetSupply: 1,         // guard: ignore accrual when net supply is dust
  maxGrowthPerStep: 1.01,  // guard: no more than +1% per step
};

for (const guard of [false, true]) {
  let m = { ...base };
  for (let step = 1; step <= 3; step++) m = accrue(m, 0.000005, { guard });
  const deposit = 5000; // what one depositor put in
  console.log(guard ? 'with guard   ' : 'without guard',
    '| index', m.index.toExponential(3),
    '| a 5,000 deposit now counts as', (deposit * m.index).toExponential(3),
    m.skipped ? `| last step skipped: ${m.skipped}` : '');
}

Output on Node 24:

without guard | index 1.581e+7 | a 5,000 deposit now counts as 7.907e+10
with guard    | index 1.000e+0 | a 5,000 deposit now counts as 5.000e+3 | last step skipped: net supply 2.0000015865662135e-8 below floor 1

Three accrual steps with a tiny amount of interest are enough to turn a 5,000 deposit into a number in the tens of billions. The net === 0 check never fires, because the net supply is positive.

The two guards

A floor on the denominator. Treat any net supply below a set amount the same as zero and skip accrual. The floor should be in the asset's own units and well above rounding noise. Note the output: floating point gives 2.0000015865662135e-8, not 2e-8, which is another reason to compare against a floor and never against an exact value.

A cap on growth per step. Even with a floor, a market can be pushed into a state where one update moves the index a lot. Refusing any single step above a set growth (1% here) turns a silent inflation into a visible halt.

Things the model leaves out

  • Real protocols use fixed-point integers. The floor still applies; pick it in base units.
  • The attacker also used a second account to liquidate each loan and get around a borrowing cap. A per-account cap does not hold against that; a per-market outflow limit per block or per hour does.
  • Retired assets deserve their own path. BUSD stopped minting in 2023 and its market here was nearly idle, which is the condition that let one loan move the denominator so far.
  • The Block saw deposit and borrow cycles in the same market from 8 November. Small repeated cycles in an idle market are worth an alert.

Kava has said the incident is contained within Kava Lend and has promised a full report. Check it when it lands, since some details here may change.

Sources: The Block, TokenPost, PANews.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.