# zkLend Exploit (February 2025)

## 1. What Happened

On February 11–12, 2025, zkLend — a decentralized lending protocol on Starknet (a ZK-rollup L2) — was exploited for approximately **$9.5 million**. Sources differ slightly: rekt.news and Halborn report **$9.57M** (~2,200 ETH plus USDC, STRK, and USDT) [1][3]; BlockSec reports **~$10M** [2]; SolidityScan reports **~$9.5M (3,600 ETH)** [4]; Verichains reports **~$9.5M** [5]. The attacker bridged ~1,801 ETH to Ethereum L1 and attempted to launder ~706.4 ETH through Railgun, which rejected the tainted funds under its exclusion policies [1][5]. On-chain evidence linked the attacker's Starknet address to two Ethereum addresses that received funds from the July 2023 EraLend reentrancy exploit, identifying the same operator [1][3]. zkLend offered a 10% white-hat bounty for return of the funds; the attacker did not respond [1][4].

## 2. Root Cause

The exploit was a **rounding error / precision loss** in zkLend's `zToken` contract, specifically in the `burn` (withdraw) function's accounting math. Three individually harmless design choices became lethal in combination: (1) accepting deposits into empty markets, (2) treating flash-loan over-repayments as "donations" that inflated the global `lending_accumulator`, and (3) integer division with downward rounding (truncation) in the scaled-balance calculation [1][2][3]. The attacker first deposited 1 wei into an empty wstETH market, then used repeated flash loans to inflate the accumulator to a target of ~4.069 × 10^18. At that accumulator value, the withdrawal calculation `amount * SCALE / accumulator` could be engineered to return exactly 1 (just enough to pass the non-zero check) while the actual wstETH withdrawn far exceeded the deposit [2][5]. Through repeated deposit-withdraw cycles the attacker grew their raw balance from 2 to 1,724 — representing over 7,000 wstETH in collateral — and borrowed all other assets against it [2][3]. zkLend had been audited twice by Nethermind (May 2022, September 2023); neither audit caught the issue [1].

## 3. Pattern

**Rounding / precision loss in accumulator-based lending math.** A lending protocol tracks user shares via a global accumulator that scales deposits. When the accumulator is manipulated to an extreme value (e.g. through donations or flash-loan interactions), the integer division in the share-to-asset conversion truncates in the user's favor. The vulnerable shape in pseudocode:

```
// Burn/zToken withdraw: scale down by accumulator
scaledDown = amount * SCALE / accumulator  // integer division, rounds down
require(scaledDown != 0)                    // only check: non-zero
// User receives 'amount' of underlying but only 'scaledDown' is deducted from shares
```

When `accumulator` is enormous relative to `amount * SCALE`, `scaledDown` equals 1 — satisfying the non-zero guard — while the user receives a withdrawal far larger than what their single share unit represents. Repeating this cycle drains the pool [2][4][5].

This is **not** a reentrancy, oracle manipulation, access-control, or key-compromise issue. It is a pure smart-contract math bug: the rounding direction benefits the attacker rather than the protocol.

## 4. The Check That Would Have Caught It

1. **Fuzz test with extreme accumulator values.** Write a fuzz test that initializes the lending accumulator at random extreme values (including 0, 1, MAX_U256, and values near the SCALE factor), then performs random sequences of deposit, borrow, repay, and withdraw. The invariant: *a user's total withdrawable amount must never exceed their deposits plus legitimately accrued interest, regardless of accumulator state.* A failure of this invariant at any accumulator value would have flagged the rounding issue immediately [2][4].

2. **Review rule: rounding must favor the protocol.** Every integer division in accounting logic that converts between shares and assets must round *against* the caller (round up on withdrawal/deposit scaling, round down on mint/redemption). Specifically: when computing `scaledDownAmount`, the division must round *up* (ceil division), so the protocol deducts at least as many shares as the withdrawn assets are worth. A reviewer checking every `div` in the zToken contract against this rule would have flagged the truncating division [3][5].

## 5. Sources

1. rekt.news — "zkLend — REKT" — https://rekt.news/zklend-rekt/
2. BlockSec — "zkLend Exploit Post-Mortem: Unraveling the Details and Clarifying Misunderstandings of the $10M Flash Loan Attack" — https://blocksec.com/zh/blog/zklend
3. Halborn — "Explained: The zkLend Hack (February 2025)" — https://www.halborn.com/blog/post/explained-the-zklend-hack-february-2025
4. SolidityScan — "zkLend Hack Analysis" — https://blog.solidityscan.com/zklend-hack-analysis/
5. Verichains — "zkLend Finance Incident Analysis" — https://blog.verichains.io/p/zklend-finance-incident-analysis
