Reproduced Exploit

Roots TroveManager — openTrove writes stale totalActiveDebt

openTrove copies totalActiveDebt into supply, then accrues interest, then stores supply + newDebt. The interest written by the accrual is thrown away. Starting from 1000 with 100 of pending interest and a new composite debt of 500, the function records 1500 and mints 500 debt tokens. The fair total…

Feb 2025•Other•logic•3 min read

Chain

Other

Category

logic

Date

Feb 2025

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: 55112-h-02-stale-totalactivedebt-used-in-opentrove-causing-incorre. Standalone Foundry PoC and full write-up: 55112-h-02-stale-totalactivedebt-used-in-opentrove-causing-incorre_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/logic/state-update · vuln/logic/incorrect-order-of-operations

Key info#

ProtocolRoots TroveManager
FindingPashov Audit Group H-02, AuditVault 55112
Vulnerable codeTroveManager.openTrove
HarmRecorded debt 1500 and 500 borrowed. Fair debt is 1600, which the fresh path rejects.
Chainlocal synthetic

TL;DR#

openTrove copies totalActiveDebt into supply, then accrues interest, then stores supply + newDebt. The interest written by the accrual is thrown away. Starting from 1000 with 100 of pending interest and a new composite debt of 500, the function records 1500 and mints 500 debt tokens. The fair total is 1600. The system cap is 1599, so a write that uses the post-interest total reverts and mints nothing.

The vulnerable code#

SOLIDITY
uint256 supply = totalActiveDebt;
_accrueActiveInterests();
uint256 base = useFreshSupply ? totalActiveDebt : supply;
// @> VULN: base is the pre-interest cache, so this sum drops the 100 interest
uint256 _newTotalDebt = base + compositeDebt;
require(_newTotalDebt + defaultedDebt <= maxSystemDebt, "Collateral debt limit reached");
totalActiveDebt = _newTotalDebt;

Inside _accrueActiveInterests, totalActiveDebt = currentDebt + activeInterests runs, and the assignment above replaces it.

Root cause#

The cache is taken before the accrual and used after it. The cap check and the stored total both miss the interest that the same transaction just added, so a trove that should hit maxSystemDebt still opens.

Preconditions#

totalActiveDebt is 1000, pending interest is 100, maxSystemDebt is 1599, and the new composite debt is 500. No price or collateral ratio is involved. The debt token balance of the manager is what gets transferred.

Attack walkthrough#

  1. openTrove(500) caches supply 1000.
  2. Accrual adds 100, so the live total is 1100.
  3. The function stores 1000 + 500 = 1500 and transfers 500 debt tokens.
  4. Fair debt is 1000 + 100 + 500 = 1600, which is above the cap.
  5. The fresh manager reverts. Its totalActiveDebt rolls back to 1000 and it pays nothing.

Diagrams#

flowchart TD A["Cache totalActiveDebt at 1000"] --> B["Accrual adds 100"] B --> C["VULN: store 1000 plus 500"] C --> D["Debt recorded as 1500 and 500 is borrowed"] D --> E["Fair total 1600 is over the cap and the fresh open reverts"]

Impact#

The system undercounts debt by the 100 of interest and still mints 500 new debt that the cap should have refused. The recorded number is 1500 against a fair 1600. Using the post-interest total blocks the open.

Remediation#

Set totalActiveDebt from the value after _accrueActiveInterests(), not from the cached supply.

How to reproduce#

BASH
cd 55112-h-02-stale-totalactivedebt-used-in-opentrove-causing-incorre_exp
forge test -vv

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.