Skip to main content

BondDepository

contracts/src/index/BondDepository.sol — a per-index, OHM-style bond market over treasury inventory. Deployed as a minimal-proxy clone by the IndexFactory and wired as the treasury's only inventory spender.

The defining property: the index token is never minted. Trade fees (per the creator's buybackShareBps) buy the index token from its pool into the treasury as inventory; a bonder deposits USDG or a basket RWA, and receives index tokens sold from that inventory at a discount, vested over a term. Because the deposit raises NAV while supply is untouched, every bond is strictly accretive to backing-per-token.

Enabled later

Bond markets are built but off by default. The v1 app hides them behind a flag; a creator (the index admin) opens them when ready. Existing indexes trade and redeem without ever touching bonds.

Opening a market (index admin)

function openMarket(address quote, uint16 discountBps, uint32 vestingSecs, uint256 capacity) external; // onlyAdmin
function closeMarket(address quote) external; // onlyAdmin
function setCapacity(address quote, uint256 capacity) external; // onlyAdmin — refresh
  • quote must be USDG or a basket stock.
  • discountBps ≤ FeeRegistry.maxBondDiscountBps (protocol cap 10%).
  • MIN_BOND_VESTING (1h) ≤ vestingSecs ≤ MAX_BOND_VESTING (30d). A non-zero minimum vesting term breaks the atomic crash-spot → bond → claim sequence, so a price manipulator must carry recovery risk across time.
  • Every basket stock must have a registered price feed — the guarded backing floor reverts on any feedless held stock, so this one-time gate prevents a feedless member being dust-donated into the treasury to brick bonding. (Baskets are immutable and feeds can't be cleared, so the check holds for the market's life.)

Bonding

function bond(address quote, uint256 amount, uint256 minPayout, address to)
external returns (uint256 noteId, uint256 payout);

Deposits amount of quote into the treasury as backing, and issues a vested claim on payout index tokens from inventory:

  1. The index must already hold real backingbackingPerTokenUsdgGuarded() must be > 0 on the pre-deposit state (else NoBacking), so a bonder can't bootstrap the floor with their own dust.
  2. The deposit is pulled to the treasury and net-measured (FoT-safe), capped at the nominal amount (guards against a rebasing quote over-crediting).
  3. payout = depositUsd / bondPrice, where bondPrice = max(marketTWAP × (1 − liveDiscount), backingPerToken) — floored at backing.
  4. Guards: payout ≥ minPayout (slippage), ≤ capacityRemaining, ≤ available inventory.
  5. The full payout is pulled from the treasury's inventory into escrow up-front (releaseBondInventory), so every note is fully collateralized the moment it exists. Nothing is minted.

The demand-driven discount

The live discount is demand-driven, OHM-style: it decays linearly from the market's configured max toward 0 as capacity is consumed.

liveDiscount = maxDiscountBps × capacityRemaining / capacity

High demand burns through capacity → discount → 0; refreshing capacity (or opening a new market) restores it. The sale price is floored at backing regardless, so the worst case is a sale at backing — accretive-neutral.

Manipulation-resistant market price

The market leg of bondPrice is a TWAP recorded by the IndexFeeHook over ≥ MIN_TWAP_WINDOW (30 min) of real time — not an instantaneous spot — so a single-tx spot crash can't set the bond price. _indexMarketPriceUsd() returns 0 (→ price at the backing floor) when:

  • the pool has too little observation history yet (freshly launched / low-activity — the discovery phase), or
  • the pool's live liquidity is below the optional FeeRegistry.minBondPoolLiquidity floor.

The sequencer guard runs first, so bonds halt while the L2 sequencer is down/recovering. Because the pool is USDG/index, the TWAP yields USDG-per-index directly — USDG ≈ $1, so no ETH/USD feed is needed.

Claiming

function claim(uint256 noteId, address to) external returns (uint256 amount);
function claimable(uint256 noteId) external view returns (uint256);
function notesOf(address owner) external view returns (uint256[] memory);

claim streams the linearly-vested portion of a note out of escrow to to. The payout was already escrowed at bond time, so claiming is a simple transfer.

Views

FunctionReturns
bondPrice(quote)Current USD price per token a bonder would pay (at the live discount)
currentDiscountBps(quote)Live demand-driven discount (bps)
availableInventory()Index tokens the treasury holds for new bonds
markets(quote)Market config: enabled, maxDiscountBps, vestingSecs, capacity, capacityRemaining

Why it can't dilute or extract

  • No mint path — payouts come from inventory the treasury already bought back; total supply is a hard invariant.
  • Floored at guarded backing — a bond can never sell inventory below true backing value, even with a manipulated market leg or a stale feed (the floor fails closed).
  • Accretive by construction — the deposit adds to NAV while supply is untouched, so backing-per-token strictly rises with each bond.