bond_servicing · Solana devnet

Technical overview

Corporate actions for tokenized bonds on Solana: coupon payment, redemption at maturity, and an issuer buyback and tender offer. Program bond_servicing, id 2UkTKYYzgZXZsganCqfxyY5Q5EeynUSBbErdKMpy8sHf, deployed on devnet.

The system services fixed-rate bonds from any market: every market-specific value (record day and time, payment time, time zone, weekend, holiday calendar, coupon basis) is a bond parameter. The sample bond is the KASE listing's example (B1: face 1,000, 10%, semi-annual, 2 years) with the Kazakh depository's rules: record list at 09:00 Almaty on the payment day, Saturday/Sunday weekend, Kazakh holiday calendar.

Detail: bond-processing.md (every instruction, every guarantee and its test), buyback-tender.md (third action).

1. Architecture

Part Where Role
On-chain program programs/bond_servicing (Anchor 1.1.2) Bond token, holder registry, positions per record moment, entitlements, vaults, payments, redemption, buyback, tender. The program is the register and the paying agent
Bond math crates/bond_math Schedule from the bond's calendar (payment and record days), day counts, amounts. Used by the program and off-chain; checked against QuantLib
Operator ops/src/scenario.ts Sends the instructions on schedule. Servicing is permissionless: any wallet can do this job (docs/devnet-evidence-keeper.md)
Reference check tools/reference/check_report.py Recomputes every amount in the operator's report with QuantLib and FinancePy
Ledger-only audit tools/audit/audit.py Reads only the public ledger (no program accounts, no operator report) and checks balances and every payment against the terms and the run's scenario (its placements, trades and offers)
Demonstration site tools/demo/build_app.py, build_stages.py, build_timeline.py, build_flow.py -> docs/demo/ (blum.caiborg.ai) Pages built only from measured devnet data; build_program.py charts the program from its source

Off-chain parts hold no rights and no keys that matter for payments: amounts and eligibility are decided on chain.

2. Record-date logic

The rule. A coupon belongs to whoever held the bond at the record moment, whatever they hold when the money arrives. Trading stays open on coupon day; it closes at the final record moment for redemption.

Record moment = record day + record time (bond-local, with the bond's UTC offset).

Positions at the record moment, without a snapshot. A program sees only the accounts passed to a transaction, so it cannot list all holders at 09:00. Instead, every balance change saves the holder's quantity first:

Between two changes a quantity is constant, so the next change saves exactly the quantity each passed record moment had. Nothing expires; nobody has to act at the record moment.

Every balance change goes through the program.

The bond's supply has the same history (Bond.supply), so an event's budget uses the supply at its record moment.

3. Entitlement calculations

All integer arithmetic in u128, rounded down, overflow reported as an error (bond_servicing/src/state.rs, bond_math).

Amount Formula
Coupon of a holder for period e position at record moment e x face x rate / periods per year (basis FixedPerPeriod), or by 30/360 (Bond Basis, European, USA) on the period's dates; rounded down once on the total position
Principal at maturity position x face x redemption price; partial redemptions use principal(redeemed + now) - principal(redeemed), so they round exactly like one redemption
Event budget the same formula on the supply at the record moment, rounded down once; the sum of holder amounts never exceeds it, and any residue stays visible in that event's vault
Buyback price per bond x quantity sold, first come first served up to the offer maximum
Tender everything tendered if the total fits the maximum; else tenders are resolved in the order they were made, each accepting floor(running total x maximum / total tendered) minus what the earlier ones accepted, so an oversubscribed offer buys exactly its maximum; paid at the offer price

Sample (the listing's example): 10 bonds at the record moment -> coupon 10 x 1,000 x 10% / 2 = 500. At maturity: 10,000 principal plus the final coupon of 500. The ledger-only audit measures this example on chain (check C6).

Amounts are checked against QuantLib and FinancePy: crates/bond_math/tests/reference.rs (fixtures generated by tools/reference/generate_fixtures.py) and, for every run, tools/reference/check_report.py.

4. Settlement flow

Each event (each coupon, and the redemption) has its own vault: a token account of the cash token whose authority is the Bond PDA. Cash in one vault can pay only that event.

Step Instruction Who What
1 create_bond registrar Mint (Token-2022, 0 decimals, extensions below), Bond account, transfer hook account list, in one transaction. Refuses cash tokens with transfer fees, transfer hooks, non-transferable extensions or a permanent delegate
2 register_holder registrar Holder registry: one HolderRecord per wallet per bond
3 place registrar Mints bonds to a registered holder, only before the first record moment and up to the issue size; refuses an issue whose budgets would not fit the cash token's u64 amounts
4 transfers holders Through the transfer hook (section 2)
5 open_event anyone, after the record moment Creates the event and its vault; budget from the supply at the record moment
6 funding issuer Cash into the event vault; simulated: the test bank mints it there (section 6)
7 confirm_funding anyone Vault >= budget -> funded; nobody is paid from an event that cannot pay everyone
8 pay_coupon anyone, any order, after the payment moment Position from the holder's history, amount from the terms; transfer from the vault and the holder's paid bit in one transaction; creates the holder's cash account if missing
9 redeem_holder anyone, after the redemption's payment moment Burns the holder's free bonds (permissioned burn) and pays their principal in one transaction
10 create_offer, funding, confirm_offer_funding issuer; confirmation by anyone Buyback or tender: price, maximum, window closing by the final record moment; own vault funded with price x maximum first
11 sell_to_offer holder Buyback: burn and pay in one transaction
12 change_tender holder Tender or withdraw during the window; tendered bonds stay in the holder's account, locked by the hook, so coupons at record moments inside the window still go to the holder
13 resolve_tender, settle_tender anyone, after the window Fix the accepted part and release the rest (no cash needed); then burn and pay the accepted part

Servicing does not depend on our operator. Steps 5, 7, 8, 9, 13 can be sent by any wallet at any later time; the amounts follow from the positions, so the sender cannot change them. Devnet run devnet-keeper: our operator stopped after coupon 0; a wallet with no role in the bond paid coupons 1-3 and redeemed everyone after the final record moment.

On-chain record of each action.

5. Solana components

Component Use
Anchor 1.1.2 program (Solana CLI 3.1.10) All instructions and accounts
Token-2022 mint with transfer hook (-> this program) Every transfer updates both holders' position histories in the same transaction
Token-2022 permanent delegate and permissioned burn (Bond PDA) Redemption and offers burn the holder's bonds with the payment; holders cannot burn outside the program
ExtraAccountMetaList (transfer hook interface) Tells Token-2022 to pass the Bond and both holder records to the hook; seeds read from the token accounts' owner field
Program-derived addresses Bond ["bond", mint], holder record ["holder", mint, owner], event ["event", bond, index], vaults ["vault", event or offer], offer ["offer", bond, id], tender ["tender", offer, owner]
Associated token accounts One bond account and one cash account per holder
SPL Token (cash) Test cash token, 6 decimals; Token-2022 cash is also supported (memo-required recipients get a Memo instruction); refused at bond creation: a transfer fee, a transfer hook, non-transferability, a permanent delegate
SPL Memo For cash recipients that require memos
Clock sysvar Bond time (section 2)

6. Implemented vs simulated

Implemented (on chain, devnet) Simulated or outside the prototype
Test bond as a Token-2022 token with the extensions above Cash rail: a test SPL token (6 decimals, mirrors USDC) minted by a test "bank" key, standing for fiat or a stablecoin
Holder registry, positions at record moments, entitlements, event vaults, coupon payment, redemption with burn, buyback, tender offer Issuer funding: the bank key mints cash into the vaults
On-chain record (state, program events, token movements) Investor onboarding and KYC: the registrar registers wallets; no identity checks
Permissionless servicing: anyone can open, confirm, pay, redeem, settle Holders: generated keypairs, labelled BH#1, BH#2, ...; trades are direct bond transfers between registered holders: no order book, and no cash leg between buyer and seller
Ledger-only audit from public ledger data Time: devnet runs compress bond time (1 bond day = 1 to 20 chain seconds); real time is 86400
No integration with KASE or the central securities depository; the Kazakh calendar and 09:00 rule are bond parameters, used as the sample

7. Evidence

Every run is on the program as deployed: a build of this repository gives it byte for byte (sha256 327271f3...). Each devnet run has its own bond and cash token.

Run (devnet) Holders Transactions Report check Ledger-only audit
devnet-crowd-1000: 600 trades, buyback, tender bought exactly its maximum (410 of 441 tendered), 4 coupons, redemption 1000 2,318 passed 3,959 of 3,959
devnet-full8: 8 trades, buyback, tender bought exactly its maximum (6 of 10 tendered), 4 coupons, redemption 8 50 passed 43 of 43
devnet-keeper: operator stops after coupon 0, an outside wallet finishes 3 25 passed 23 of 23
devnet-hostile: an attacker at every step; 35 of 35 attempts refused, each a failed transaction 6 53 passed 35 of 35

8. Limits