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 backing — backingPerTokenUsdgGuarded() 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.