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…
Chain
Other
Category
logic
Date
Apr 2023
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
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.
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-registrymirror.
Vulnerability classes: vuln/logic/liquidation-logic · vuln/dos/frozen-funds · vuln/logic/missing-check
Key info#
| Protocol | Term Finance |
| Finding | Sigma Prime term1, AuditVault 26294 |
| Vulnerable code | BatchLiquidation._withinNetExposureCapOnLiquidation |
| Harm | The liquidation reverts and 1000 COLL stays in the locker |
| Chain | local 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#
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#
- 1000e18 collateral is minted into the locker.
batchLiquidationzeros the obligation and attempts the seize.- The cap check reads a 0 obligation and divides by it.
- The call reverts. The locker still holds 1000e18 and the liquidator holds 0.
Diagrams#
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#
- AuditVault finding: https://github.com/Auditware/AuditVault/blob/main/findings/26294-liquidations-fail-with-division-by-zero-errors-sigmaprime-no.md
- Sigma Prime report: https://github.com/sigp/public-audits/blob/master/reports/term-finance/term1/review.pdf
- Audited commit: term-finance
e57734f
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 26294-liquidations-fail-with-division-by-zero-errors-sigmaprime-no_exp (evm-hack-registry mirror).
- AuditVault finding: 26294-liquidations-fail-with-division-by-zero-errors-sigmaprime-no.
Alerts & third-party analyses
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.