§2 · The protocol
How it works
Wrap and unwrap in kind, pro-rata against the vault, with every rounding in the pod's favour and no oracle anywhere.
A pod keeps books. For each component it records a tracked_balance: the raw units it holds on behalf of the cohort supply. Every wrap and unwrap is priced against those books and nothing else. There is no price input anywhere in the program.
Wrap: deposit in kind§
To wrap, you name a gross amount of cohorts, pod_amount, and deposit every component in proportion to what the pod already holds:
amount_in[i] = ceil( pod_amount × tracked_balance[i] ÷ supply )The pod then mints pod_amount cohorts: the wrap fee to its FeeVault and the rest to you. The fee is ceil(pod_amount × wrap_fee_bps ÷ 10,000), so you receive pod_amount − fee. You set a ceiling for each component, max_in, and the wrap fails with SlippageExceededIn if the pod would take more.
Before the first cohort exists the pod has no supply to divide by, so the recipe stands in for it: amount_in[i] = ceil(pod_amount × recipe_units[i] ÷ 10⁹), where recipe_units is the raw amount of each component behind one whole cohort. The creator's genesis wrap inside create_pod works this way. It must mint at least one whole cohort, and it is the only wrap that pays no fee.
Unwrap: redeem in kind§
To unwrap, you return pod_amount cohorts. The unwrap fee, ceil(pod_amount × unwrap_fee_bps ÷ 10,000), moves to the FeeVault; the rest, net, is burned. The pod pays out every component:
amount_out[i] = floor( net × tracked_balance[i] ÷ supply )supply here is the supply before the burn. You set a floor for each component, min_out, and the unwrap fails with SlippageExceededOut if any payout would fall below it.
An unwrap pays every component or none. If one component can't move because its issuer paused or froze it, a plain unwrap fails. unwrap_partial exists for that case: §6 Partial unwrap and claim tickets.
Books, not balances§
The program never prices against the balance it reads from a vault. Anyone can send tokens straight to a vault; if that changed the exchange rate, a donation could be used to manipulate it, which is a known attack on vault tokens. So a deposit only counts once the program has moved it in through wrap and recorded it.
The vault balance still matters in one direction. If a vault holds less than the books say (tracked_balance + claims_outstanding), an issuer has taken tokens out with its permanent delegate. The program records the gap as shortfall, lowers tracked_balance by it, and every holder of that pod shares the loss pro-rata. If a vault holds more, the surplus is ignored.
A worked example§
Take a pod shaped like cCORE, with three components that each have 8 decimals. It minted 1,000 cohorts at genesis; 0.6 of them have since been burned by fee distributions, so each cohort is backed by slightly more than the recipe. Wrap and unwrap fees are 30 bps.
The pod's books
- Cohort supply
- 999.4 cohorts
- SPYx tracked
- 40 SPYx
- QQQx tracked
- 30 QQQx
- GLDx tracked
- 35 GLDx
- Wrap and unwrap fee
- 30 bps each
999,400,000,000 raw
recipe 0.04 per cohort · backing now 0.04002401
recipe 0.03 per cohort · backing now 0.03001801
recipe 0.035 per cohort · backing now 0.03502101
Wrapping 10 cohorts gross costs each component's current backing, rounded up, and mints 9.97 to you:
Wrap 10 cohorts (gross)
- You deposit SPYx
- 0.40024015
- You deposit QQQx
- 0.30018011
- You deposit GLDx
- 0.35021013
- Fee to the FeeVault
- 0.03 cohorts
- Minted to you
- 9.97 cohorts
ceil(10,000,000,000 × 4,000,000,000 ÷ 999,400,000,000) = 40,024,015
ceil(10,000,000,000 × 3,000,000,000 ÷ 999,400,000,000) = 30,018,011
ceil(10,000,000,000 × 3,500,000,000 ÷ 999,400,000,000) = 35,021,013
ceil(10,000,000,000 × 30 ÷ 10,000) = 30,000,000
Unwrapping 10 cohorts pays out against the same books, rounded down:
Unwrap 10 cohorts
- Fee to the FeeVault
- 0.03 cohorts
- Burned
- 9.97 cohorts
- You receive SPYx
- 0.39903942
- You receive QQQx
- 0.29927956
- You receive GLDx
- 0.34915949
ceil(10,000,000,000 × 30 ÷ 10,000)
net = amount − fee
floor(9,970,000,000 × 4,000,000,000 ÷ 999,400,000,000) = 39,903,942
floor(9,970,000,000 × 3,000,000,000 ÷ 999,400,000,000) = 29,927,956
floor(9,970,000,000 × 3,500,000,000 ÷ 999,400,000,000) = 34,915,949
Wrapping 10 and unwrapping 10 doesn't return what you put in. Two fees of 0.03 cohorts each stayed behind in the FeeVault, and the rounding on each side kept a few raw units in the vaults.
Rounding in the pod's favour§
Every division in the program rounds so that the pod, and so the remaining holders, never lose a raw unit:
| Quantity | Direction | Who absorbs the remainder |
|---|---|---|
| Components in on a wrap | up | the depositor |
| Wrap fee | up | the depositor |
| Unwrap fee | up | the redeemer |
| Components out on an unwrap | down | the redeemer |
| Creator and protocol fee payouts | down | burned with the holder share |
At normal sizes this is invisible. At the edges it decides the outcome:
Rounding at the edges (raw units)
- Wrap 1 raw unit of cohort
- pays 1 / 1 / 1 raw
- Unwrap 1,000 raw units
- fee 3, SPYx 3
- Unwrap 1 raw unit
- rejected
SPYx / QQQx / GLDx: each rounds up to at least 1 raw unit, while the fee rounds up to 1 and you receive 0
exact share 3.990 raw SPYx; you get 3 and the remainder stays in the vault
the fee rounds up to 1 raw unit and leaves nothing to burn, so the program fails it with UnwrapTooSmall
The program's test suite checks that random sequences of wraps and unwraps never lower the backing per cohort.
Why there is no oracle§
An oracle answers the question "what is this basket worth?" A pod never needs to ask it. Wrapping and unwrapping exchange the basket for cohorts at the pod's own book rate, and the book rate is just vault contents divided by supply. A price feed could only be wrong, stale or attacked; leaving it out removes a failure that no amount of monitoring fixes.
The price of a cohort on a DEX still matters to anyone trading it, and nothing in the program sets it. Arbitrage does.
Arbitrage keeps the price near NAV§
The net asset value (NAV) of a cohort is its backing priced at the components' own market prices. Suppose a cohort's pool on a Solana DEX trades above NAV. An arbitrageur buys the components, wraps them, sells the new cohorts into the pool and keeps the difference; selling pushes the pool down. If the pool trades below NAV, they buy cohorts, unwrap them and sell the components; buying pushes the pool up.
The band's width is the round-trip cost of the trade. Cohort's economics simulation (docs/economics.md) measured it from live Solana liquidity for the cMAG7 and cCORE components:
- With 30 bps wrap and unwrap fees and a $1M cohort/USDC pool, an arbitrage breaks even at about 75 bps of mispricing: the 25 bps pool fee, the 30 bps wrap or unwrap fee, roughly 8–15 bps of component slippage and 2 bps per leg of overhead.
- In a year of simulated trading the pool sat 38 bps from NAV on average, and within 68 bps 95% of the time.
- Solana transaction fees and Jito tips came to about $0.02 per arbitrage bundle, which doesn't matter.
Both legs of the trade are on-chain, so arbitrage doesn't wait for the stock market: the arbitrageur prices against the components' own pools, which trade around the clock. That is why a cohort can trade at a sensible price on a Saturday.