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.
- Attacker acquires tokens on the low-backing chain
L.
- 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.
- 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.
- If
V_R / S_R > V_L / S_L, then b > a; the difference is extracted from R's terminal balance.
- 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.
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 setscopeCashOutsToLocalBalances: 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.solprepare(...)is permissionless for any project-token holder._pullBackingAssets(...)._pullBackingAssets(...)cashes out from the terminal withholder = address(this).revnet-core-v6/src/REVOwner.solbeforeCashOutRecordedWith(...)checks_isSuckerOf(...)before cash-out delay, remote supply/surplus aggregation, revnet fee handling, and buyback routing.(0, context.cashOutCount, localSupply, localSurplus, emptySpecs).revnet-core-v6/src/REVLoans.sol_borrowableAmountFrom(...)uses cross-chain aggregate supply/surplus whenscopeCashOutsToLocalBalances == false, then caps only by local surplus.Exploit Path
Assume two chains for the same revnet have different local backing per token.
L.sucker.prepare(...)fromL -> R.L.a = V_L * c / S_Lbacking and mints the samectokens on rich chainR.sucker.prepare(...)fromR -> L.R.b = (V_R + a) * c / (S_R + c)backing.V_R / S_R > V_L / S_L, thenb > a; the difference is extracted fromR's terminal balance.L, then borrows against the now over-backed local tokens viaREVLoans.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:
100total supply,1000 ETHsurplus.100 ETH, total supply becomes100, attacker owns62.preparelow -> rich moves62 ETH.preparerich -> low moves about406.44 ETH.62tokens on low chain backed by about444.44 ETH.REVLoans.borrowFrom(...)against those62tokens returns about317.47 ETHgross.298.42 ETH.Net profit is about
198 ETHagainst a100 ETHstarting 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:
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.scopeCashOutsToLocalBalances: trueif 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.