[High] Treasury: bond payments never raise totalReserves → deterministic bond-market halt

Summary

Bond payments in BondDepository.deposit go directly to the TREASURY
address via safeTransfer/safeTransferFrom never through
Treasury.deposit(), the only function that raises totalReserves (besides
governor-only auditReserves()). But every bond’s addNote calls
Treasury.mint(payout) (src/types/NoteKeeper.sol:49), which requires
amount <= excessReserves() where
excessReserves() = totalReserves - (FTSO.totalSupply() - totalDebt)
(src/Treasury.sol:260; totalDebt is never written, always 0).

So each bond lowers excessReserves by exactly its payout while
totalReserves stagnates. Once cumulative payouts exceed the initial buffer,
every new bond reverts with InsufficientReserves a deterministic,
permanent halt of the core bond market until governor/guardian calls
auditReserves(). The treasury demonstrably holds the bond payments the whole
time: pure accounting drift.

Severity

High. Complete, deterministic denial of the protocol’s core bond-issuance
function; recovery is gated on a trusted role. No fund loss, but the market
bricks itself through normal use.

Steps to Reproduce

  1. Seed reserves properly: treasury.deposit(reserve, X, X_value/2) →
    excessReserves = E0 > 0.
  2. Execute bonds. After each bond assert:
    treasury.totalReserves() unchanged, ftso.totalSupply() up by payout,
    treasury.excessReserves() down by exactly payout.
  3. Keep bonding; eventually depository.deposit(...) reverts
    InsufficientReserves() from Treasury.mint inside addNote.
  4. Confirm weth.balanceOf(treasury) > 0 (funds present — accounting only).
  5. vm.prank(governor); treasury.auditReserves(); → bonds work again.

Proof of Concept

Foundry test against the actual contracts (read-only reference). Full PoC
source available on request.

cd poc-foundry && forge test --match-test 'test_GH08' -vv
# PASS test_GH08_bondDoesNotRaiseTotalReserves  (mechanism: -payout per bond)
# PASS test_GH08_bondDoSUntilAudit              (drain loop -> revert -> audit recovery)

Key assertions from the passing tests:

// after one real bond:
assertEq(treasury.totalReserves(), reservesBefore);          // flat
assertEq(treasury.excessReserves(), excessBefore - payout);   // drained exactly
// after the drain loop:
vm.expectRevert();
depository.deposit{value: amount}(drainId, amount, type(uint256).max, ALICE); // InsufficientReserves
assertGt(weth.balanceOf(address(treasury)), 0);               // funds are there

Impact

The bond market has a deterministic expiry measured in cumulative payout
volume. In production this reads as “bonds randomly stop working”; the only
fix is a trusted-role auditReserves(), i.e. recurring manual intervention
for a core function. It also makes buyBack() underpay while drifted.

Remediation

Route bond payments through Treasury.deposit() (or otherwise credit
totalReserves) instead of transferring directly to the TREASURY address.
Short-term: document the required auditReserves() cadence and consider
making it permissionless-but-rate-limited, since the values it reads are
on-chain anyway.

Thank you for your report!

It looks like you’ve spotted an issue inherited from Olympus DAO V2. This is more a matter of monetary policy and game theory than of technology. As I understand it, the intent is to ensure that the backing price - that is, the risk-free value (RFV) - is not affected by bond purchases, so that user actions don’t mix into it.

For that reason, there is an auditReserves function that acts as a governance trigger to recalculate the backing price. If you see a more in-depth or complex abuse of this mechanism, please continue the thread.

Thanks for the thoughtful response! Two data points, respectfully:

1. On the Olympus V2 inheritance: I checked the Version-2 branch of OlympusDAO/olympus-contracts directly. There, Treasury.mint is indeed gated on excessReserves() (and restricted to REWARDMANAGER) but BondDepository.sol contains zero mint() calls, so V2’s bond flow never touches that gate. In GHOST, NoteKeeper.addNote calls _TREASURY.mint on every bond, which is what routes the bond flow through the gate. So the toxic combination intake bypassing Treasury.deposit() plus payout gated on excessReserves() is GHOST’s own composition, not a faithful inheritance. (If you mean a different V2 lineage, happy to be pointed at the exact contract.)

2. On intent vs. effect: if the goal is “RFV unaffected by bond purchases,” the accounting should credit reserves by the intake amount (that’s what Treasury.deposit() is for) then RFV moves only by the protocol’s discount profit per bond, which is the actual Olympus model. The current code does the opposite: reserves flat, supply up, so RFV strictly decays with every bond until InsufficientReserves bricks the market. The mechanism achieves the inverse of the stated intent.

3. The deeper abuse you asked for: the drain is attacker-accelerable and profitable. Each bond earns the bonder the discount while reducing excessReserves by the full payout, so a griefer can deterministically halt the bond market for all users at net profit, with recovery gated on a trusted auditReserves() call. Core-market liveness depending on manual governance intervention is an availability issue with a concrete abuse path.

Suggested fix stays minimal: route bond intake through Treasury.deposit() (or otherwise credit totalReserves). Happy to demonstrate end-to-end on testnet if useful.