Skip to content

Cross-chain revnets can leak value via local-only sucker prepare cash-outs #159

Description

@livid

Summary

Cross-chain revnets can leak value through the sucker bridge path because registered sucker cash-outs are handled as local-only, zero-tax withdrawals, while normal holder cash-outs and loans use the unscoped cross-chain aggregate state.

For projects deployed by deploy-all-v6/script/Deploy.s.sol, projects 1, 2, 3, 4, 5, and 7 use _buildSuckerConfig(...) and set scopeCashOutsToLocalBalances: false. ART/project 6 is Base-only and has no suckers, so it is not needed for this attack.

Affected Code

  • nana-suckers-v6/src/JBSucker.sol

    • prepare(...) is permissionless for any project-token holder.
    • It transfers the holder's ERC20 project tokens into the sucker, then calls _pullBackingAssets(...).
    • _pullBackingAssets(...) cashes out from the terminal with holder = address(this).
  • revnet-core-v6/src/REVOwner.sol

    • beforeCashOutRecordedWith(...) checks _isSuckerOf(...) before cash-out delay, remote supply/surplus aggregation, revnet fee handling, and buyback routing.
    • For registered suckers it returns (0, context.cashOutCount, localSupply, localSurplus, emptySpecs).
  • revnet-core-v6/src/REVLoans.sol

    • _borrowableAmountFrom(...) uses cross-chain aggregate supply/surplus when scopeCashOutsToLocalBalances == false, then caps only by local surplus.

Exploit Path

Assume two chains for the same revnet have different local backing per token.

  1. Attacker acquires tokens on the low-backing chain L.
  2. Attacker calls sucker.prepare(...) from L -> R.
    • This uses local-only, zero-tax sucker cash-out on L.
    • It moves a = V_L * c / S_L backing and mints the same c tokens on rich chain R.
  3. Attacker calls sucker.prepare(...) from R -> L.
    • This now uses local-only, zero-tax sucker cash-out on R.
    • It moves b = (V_R + a) * c / (S_R + c) backing.
  4. If V_R / S_R > V_L / S_L, then b > a; the difference is extracted from R's terminal balance.
  5. Attacker claims back on L, then borrows against the now over-backed local tokens via REVLoans.borrowFrom(...).

The attacker does not need project-owner permissions, a compromised operator, or a malicious bridge. They only need project tokens and access to the canonical registered suckers.

Concrete Arithmetic

Using a revnet with 38% reserved rate and 10% cash-out tax:

  • Rich chain: 100 total supply, 1000 ETH surplus.
  • Low chain: attacker pays 100 ETH, total supply becomes 100, attacker owns 62.
  • prepare low -> rich moves 62 ETH.
  • prepare rich -> low moves about 406.44 ETH.
  • Attacker now has 62 tokens on low chain backed by about 444.44 ETH.
  • REVLoans.borrowFrom(...) against those 62 tokens returns about 317.47 ETH gross.
  • After 2.5% protocol fee, 1% REV fee, and 2.5% source fee, attacker receives about 298.42 ETH.

Net profit is about 198 ETH against a 100 ETH starting payment, funded by the richer chain's treasury.

Impact

For unscoped cross-chain revnets with suckers, local treasury balances can be drained whenever per-chain backing diverges. The exploit can be repeated until local balances converge or are depleted enough that fees/gas dominate.

This affects deploy-all projects 1, 2, 3, 4, 5, and 7. Project 6 / ART is Base-only with no suckers in the current deploy script.

Suggested Fix Direction

The invariant needs one accounting model for value-moving exits:

  • Either sucker prepare(...) should use the same cross-chain aggregate supply/surplus and fees/tax semantics as ordinary holder cash-outs, with a separate bridge-accounting mechanism to prevent remote double counting.
  • Or unscoped revnets should not allow local pro-rata sucker prepares when local backing per token diverges from global backing.
  • Or deploy these revnets with scopeCashOutsToLocalBalances: true if bridge exits are intended to be local-only.

At minimum, add a regression test with two chain snapshots where local backing ratios differ, then round-trip tokens through suckers and assert that the attacker cannot increase withdrawable/borrowable value net of fees.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions