Reproduced Exploit

Term Finance — a complete liquidation divides by zero and 1000 collateral stays

A complete liquidation calls liquidatorCoverExposure, which sets the repurchase obligation to 0, and then divides excess equity by the USD value of that obligation. The divisor is 0, so the transaction reverts and the seize of 1000 collateral is undone. The control returns true when the obligation…

Apr 2023•Other•logic•3 min read

Chain

Other

Category

logic

Date

Apr 2023

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.

Loading fork state…

Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 26294-liquidations-fail-with-division-by-zero-errors-sigmaprime-no. Standalone Foundry PoC and full write-up: 26294-liquidations-fail-with-division-by-zero-errors-sigmaprime-no_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/logic/liquidation-logic · vuln/dos/frozen-funds · vuln/logic/missing-check

Key info#

ProtocolTerm Finance
FindingSigma Prime term1, AuditVault 26294
Vulnerable codeBatchLiquidation._withinNetExposureCapOnLiquidation
HarmThe liquidation reverts and 1000 COLL stays in the locker
Chainlocal synthetic

TL;DR#

A complete liquidation calls liquidatorCoverExposure, which sets the repurchase obligation to 0, and then divides excess equity by the USD value of that obligation. The divisor is 0, so the transaction reverts and the seize of 1000 collateral is undone. The control returns true when the obligation is already 0, and the liquidator receives the 1000.

The vulnerable code#

SOLIDITY
uint256 getBorrowerRepurchaseObligation = servicer.getBorrowerRepurchaseObligation(who);
uint256 excessEquity = 1 ether;
uint256 denom = oracle.usdValueOfTokens(getBorrowerRepurchaseObligation);
// @> VULN: a complete liquidation leaves the repurchase obligation at 0, so this divides by zero
uint256 ratio = excessEquity / denom;

The control returns true before the division when the obligation is 0.

Root cause#

The cap check assumes a positive repurchase obligation. The function that runs immediately before it, on a complete liquidation, sets that obligation to 0. Solidity 0.8 reverts on division by zero, so the liquidation cannot complete.

Preconditions#

The locker holds 1000e18 collateral. The borrower has a positive obligation at the start of the call. batchLiquidation covers the full exposure and then reads the obligation back. No time warp is required.

Attack walkthrough#

  1. 1000e18 collateral is minted into the locker.
  2. batchLiquidation zeros the obligation and attempts the seize.
  3. The cap check reads a 0 obligation and divides by it.
  4. The call reverts. The locker still holds 1000e18 and the liquidator holds 0.

Diagrams#

flowchart TD A["Locker holds 1000 collateral"] --> B["Cover exposure sets the obligation to 0"] B --> C["Seize is attempted"] C --> D["VULN: divide by the zero obligation"] D --> E["Liquidation reverts and 1000 stays"]

Impact#

The 1000e18 collateral cannot be seized. A complete liquidation is impossible, so the underwater position stays open and the collateral stays in the locker. The control pays the liquidator the full 1000e18 and clears the debt.

Remediation#

Return true at the start of _withinNetExposureCapOnLiquidation when the repurchase obligation is 0. Sigma Prime retested this as fixed on commit e883f0e.

Sources#


Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

  • Web3Sec X hacked database: search.
  • Rekt leaderboard: search.
  • Solodit incident search: search.

These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.