Reproduced Exploit

Roots — surplus collateral claim reverts and 6 stays in the staker

A partial liquidation records surplusBalances[borrower] = 6 but the collateral itself is staked. claimCollateral zeroes the record and then transfers from the trove manager. The trove manager's balance is 0, so the transfer reverts and the whole claim rolls back. The 6 stays in the staker and the b…

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: 55114-h-04-borrowers-fail-to-claim-their-surplus-collateral-pashov. Standalone Foundry PoC and full write-up: 55114-h-04-borrowers-fail-to-claim-their-surplus-collateral-pashov_exp in the evm-hack-registry mirror.


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

Key info#

ProtocolRoots TroveManager
FindingPashov H-04, AuditVault 55114
Vulnerable codeclaimCollateral transfers the collateral token from the trove manager
Harm6 surplus collateral stays in the staker. The borrower receives 0.
Chainlocal synthetic

TL;DR#

A partial liquidation records surplusBalances[borrower] = 6 but the collateral itself is staked. claimCollateral zeroes the record and then transfers from the trove manager. The trove manager's balance is 0, so the transfer reverts and the whole claim rolls back. The 6 stays in the staker and the borrower receives nothing. Releasing the tokens with staker.onWithdrawal pays the borrower 6.

The vulnerable code#

SOLIDITY
function claimCollateral(address receiver) external {
    uint256 claimableColl = surplusBalances[msg.sender];
    require(claimableColl > 0, "No collateral available to claim");
    surplusBalances[msg.sender] = 0;
    // @> VULN: the collateral is staked, so this transfer pulls from the TroveManager and reverts.
    collateralToken.transfer(receiver, claimableColl);
}

Root cause#

The surplus number is stored on the trove manager. The tokens are not. They were deposited into the staker. A plain ERC-20 transfer uses the caller's balance, which is the trove manager's, and that balance is 0.

Preconditions#

The borrower has a recorded surplus of 6. The staker holds those 6 tokens. The trove manager holds 0.

Attack walkthrough#

  1. 6 collateral is minted to the staker. addCollateralSurplus records 6 for the borrower.
  2. The borrower calls claimCollateral.
  3. transfer from the trove manager reverts. The surplus record rolls back.
  4. The staker still holds 6. The borrower holds 0.

The control calls staker.onWithdrawal, which transfers the 6 from the staker to the borrower.

Diagrams#

flowchart TD A["Staker holds 6, surplus record is 6"] --> B["Borrower calls claimCollateral"] B --> C["VULN: trove manager transfers a 0 balance"] C --> D["Call reverts and the 6 stays"] D --> E["onWithdrawal pays the borrower 6"]

Impact#

The liquidated borrower cannot collect surplus collateral. 6 tokens remain in the staker.

Remediation#

Withdraw the surplus from the staker, then send it to the borrower. Do not transfer it from the trove manager.

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.