Read from devnet · 8 holders, 50 transactions
What runs on Solana, step by step
Every step of the bond's life is one Solana transaction. For each: who may send it, which programs ran in which order (a program calling another is indented under it), every account each one used with its role, what the program checks before it acts, and which balances changed. Each example is a real transaction of the 8-holder devnet run.
Every bond movement runs through bond_servicing
One row per step, top to bottom in the bond's order. Columns: who may send the step, then the programs. Arrows: the calls between them in the order the runtime ran them, read from the transactions below; an arrow into bond_servicing is green. In a transfer, the holder calls Token-2022 and Token-2022 calls bond_servicing back: the arrow pointing left. In this run the registrar's wallet paid every fee.
bond_servicing writesBond accountHook account list
not drawnSystem program (3)
bond_servicing writesHolder record · BH#1Bond account
not drawnSystem program (2), Associated Token Account program (2), and the 3 calls they made
bond_servicing writesOffer 0 · buyback
not drawnSystem program (2)
bond_servicing writesOffer 0 · buyback
bond_servicing writesHolder record · BH#1Holder record · BH#3
bond_servicing writesBond accountHolder record · BH#2Offer 0 · buyback
bond_servicing writesEvent 0 · coupon
not drawnSystem program (2)
bond_servicing writesEvent 0 · coupon
bond_servicing writesHolder record · BH#1
bond_servicing writesOffer 1 · tenderHolder record · BH#1Tender · BH#1
not drawnSystem program (1)
bond_servicing writesOffer 1 · tenderHolder record · BH#1Tender · BH#1
bond_servicing writesBond accountHolder record · BH#1Offer 1 · tenderTender · BH#1
bond_servicing writesBond accountHolder record · BH#1
The parts
walletprogrambond_servicing accounttoken account or mintrolesignswritesHover a name for its address; click it to open it in Solana Explorer.
- bond_servicingbond_servicing: this program: the bond's rules
- Token-2022Token-2022: runs the bond token; calls bond_servicing on every bond transfer
- SPL TokenSPL Token: runs the cash token
- Associated Token Account programAssociated Token Account program: opens a wallet's token account for a mint
- System programSystem program: creates accounts and moves SOL for their rent
- Compute Budget programCompute Budget program: sets the transaction's compute limit; no part of the bond's logic
- Memo programMemo program: adds a memo to a payment when the receiving cash account requires one
- Bond accountBond account: ["bond", bond token]: terms, time table, supply history; signs as mint, burn and vault authority
- Hook account listHook account list: ["extra-account-metas", bond token]: tells Token-2022 which accounts the hook needs
- Holder record · BH#1Holder record · BH#2Holder record · BH#3Holder record · BH#4Holder record · BH#5Holder record · BH#7Holder records: ["holder", bond token, wallet]: position history, paid bits, locked and redeemed bonds
- Event 0 · couponEvent 4 · redemptionEvents: ["event", bond, index]: one per coupon and the redemption: supply at the record moment, budget, vault, funded
- Offer 1 · tenderOffer 0 · buybackOffers: ["offer", bond, id]: kind, price, maximum, window, total, vault, tender counters
- Tender · BH#1Tenders: ["tender", offer, wallet]: number in the offer, quantity, accepted, settled
- Bond token (mint)Bond token (mint): Token-2022; transfer hook = bond_servicing; mint authority, permanent delegate and burn authority = bond account; no freeze authority
- Cash token (mint)Cash token (mint): SPL Token, 6 decimals; the bank mints it
- BH#1 · bondsBH#2 · bondsBH#3 · bondsBH#4 · bondsBond accounts: Token-2022 associated accounts: one per holder
- BH#1 · cashBH#2 · cashBH#3 · cashBH#4 · cashBH#5 · cashBH#7 · cashCash accounts: SPL Token associated accounts: where coupons and principal arrive
- Vault · coupon 0Vault · redemptionVault · offer 1 (tender)Vault · offer 0 (buyback)Vaults: ["vault", event or offer]: cash accounts whose authority is the bond account
create_bond
may be sent by: RegistrarThe registrar creates the bond in one transaction: the bond token, the bond account that holds the terms and the time table, and the list of accounts the transfer hook needs. The program makes the bond account the token's mint authority, permanent delegate and burn authority, sets itself as the transfer hook, and gives the token no freeze authority.
- 1bond_servicingCreateBond59,688 CUregistrar: Registrar signs writesmint: Bond token (mint) signs writesbond: Bond account writescash_mint: Cash token (mint)extra_account_meta_list: Hook account list writestoken_program: Token-2022system_program: System program
- BondCreated: 4 coupon periods
- 2System programcreateAccount
- 2System programcreateAccount
- 2System programcreateAccount
- 2Token-2022InitializePermanentDelegate1,272 CU
- 2Token-2022initializeTransferHook1,384 CU
- 2Token-2022PermissionedBurnExtension1,537 CU
- 2Token-2022InitializeMint22,437 CU
InvalidTermsface value, issue size, rate, redemption price, period and time compression above zero; record and payment times inside a day; payments a year x months a period = 12InvalidSchedulepayment and record days match the schedule computed from issue and maturity: business days, payment at most 14 days from the period end, record moments in order and not after payment, at most 40 periodsUnsupportedCashTokenthe cash token is SPL Token, or Token-2022 without a transfer fee, transfer hook, non-transferability or permanent delegateaccount existsthe bond account and the hook list are new accounts at their seeds
No token balance changed: only program accounts were written.
register_holder + place
may be sent by: RegistrarThe registrar admits a wallet (KYC is simulated) and places bonds with it: a holder record is created for the wallet, its bond account is opened, and the program mints the bonds into it, signing as mint authority. The holder record and the bond's supply count the new bonds.
- 1bond_servicingRegisterHolder12,796 CUregistrar: Registrar signs writesbond: Bond accountholder: Holder record · BH#1 writessystem_program: System program
- HolderRegistered: BH#1
- 2System programcreateAccount
- 1Associated Token Account programcreateIdempotent21,779 CU
- 2Token-2022GetAccountDataSize1,609 CU
- 2System programcreateAccount
- 2Token-2022InitializeImmutableOwner734 CU
- 2Token-2022InitializeAccount33,634 CU
- 1Associated Token Account programcreateIdempotent4,437 CU
- 1bond_servicingPlace27,505 CUregistrar: Registrar signsbond: Bond account writesmint: Bond token (mint) writesholder: Holder record · BH#1 writesholder_bonds: BH#1 · bonds writestoken_program: Token-2022
- BondsPlaced: 10 bonds to BH#1
- 2Token-2022MintTo2,288 CU
The same transaction also ran RegisterHolder + Place for BH#2; RegisterHolder for BH#3; only BH#1's RegisterHolder + Place is drawn. Balances are the whole transaction's.
ConstraintHasOneonly the bond's registrar registers and placesaccount existsone holder record per wallet and bond, at ["holder", bond token, wallet]PlacementClosedplacement only before the first record momentExceedsIssueSizesupply + new bonds within the issue size from the termsIssueTooLargeevery coupon and the principal of the new supply fit in a token amount
| Account | Before | After | Change |
|---|---|---|---|
| BH#2 · bonds | 0 | 5 | +5 |
| BH#1 · bonds | 0 | 10 | +10 |
create_offer
may be sent by: RegistrarThe registrar opens a buyback (first come) with a price, a maximum and a window. The program creates the offer and a vault for its cash, a cash account whose authority is the bond account.
- 1bond_servicingCreateOffer24,177 CUregistrar: Registrar signs writesbond: Bond accountoffer: Offer 0 · buyback writescash_mint: Cash token (mint)vault: Vault · offer 0 (buyback) writescash_token_program: SPL Tokensystem_program: System program
- OfferCreated: offer 0, buyback, up to 4 at 990.00
- 2System programcreateAccount
- 2System programcreateAccount
- 2SPL TokeninitializeAccount3233 CU
ConstraintHasOneonly the bond's registrar opens offersInvalidOfferprice and maximum above zero; the window opens before it closes and closes by the final record moment; price x maximum fits
No token balance changed: only program accounts were written.
the bank pays into the offer's vault
may be sent by: BankNot a bond_servicing instruction. The bank, the cash token's mint authority, credits the vault: an SPL Token MintTo. In real markets this is the issuer's payment to its paying agent; here the bank stands for it.
- 1SPL TokenmintTo120 CU
owner does not matchSPL Token: only the cash token's mint authority mints
| Account | Before | After | Change |
|---|---|---|---|
| Vault · offer 0 (buyback) | 0.00 | 3,960.00 | +3,960.00 |
confirm_offer_funding
may be sent by: AnyoneAnyone confirms that the vault holds price x maximum. Only then may holders sell or tender.
- 1bond_servicingConfirmOfferFunding5,109 CU
- OfferFunded: offer 0, vault 3,960.00 of 3,960.00
AlreadyFundednot confirmed beforeInsufficientFundingthe vault holds at least price x maximum
No token balance changed: only program accounts were written.
record moment: no transaction
may be sent by: NobodyNothing is sent at a record moment, and no program runs: a Solana program runs only inside a transaction someone sends. This bond's record moments are 2026-09-07 09:00, 2027-03-09 09:00, 2027-09-06 09:00, 2028-03-06 09:00 (bond time, UTC+5). Each holder record keeps a position history: the quantity now, live_from (how many record moments had passed at its last change) and at[e], the quantity saved for record moment e. The next change to a holding, a transfer, a sale or a redemption, first copies the current quantity into at[] for every record moment passed since live_from. Between two changes a holding is constant, so that copy is exactly the holding at those moments, however late anyone looks.
- PositionsSaved: BH#1 held 10 at record moment 0
- PositionsSaved: BH#3 held 0 at record moment 0
A coupon is paid on at_moment(e): at[e] if the holding changed after moment e, else the quantity now. A holder that never trades needs no transaction at all.
transfer (Token-2022) -> transfer_hook
may be sent by: HolderA holder sends bonds with Token-2022's TransferChecked. Token-2022 moves the balances, then calls bond_servicing's transfer hook with the accounts listed at bond creation. The hook keeps each holder's quantity for every record moment passed since its last change (PositionsSaved), then records the new quantities. This is how the register knows who held what at 09:00 without anyone taking a list.
- 1Token-2022TransferChecked51,264 CU
- 2bond_servicingTransferHook28,786 CUsource_token: BH#1 · bondsmint: Bond token (mint)destination_token: BH#3 · bondsauthority: BH#1extra_account_meta_list: Hook account listbond: Bond accountsource_holder: Holder record · BH#1 writesdestination_holder: Holder record · BH#3 writes
- PositionsSaved: BH#1 held 10 at record moment 0
- PositionsSaved: BH#3 held 0 at record moment 0
NotTransferringruns only inside a real Token-2022 transferNotAssociatedTokenAccountboth sides are the holders' associated bond accountsAccountOwnedByWrongProgramboth wallets have holder records (are registered)TransfersClosedAtMaturitybefore the final record momentPositionMismatcheach holder record agrees with its balance before the transferBondsLockedthe seller keeps at least its tendered bonds
| Account | Before | After | Change |
|---|---|---|---|
| BH#3 · bonds | 0 | 3 | +3 |
| BH#1 · bonds | 10 | 7 | -3 |
sell_to_offer
may be sent by: HolderA holder sells bonds to the buyback. In one transaction the program burns them (Token-2022 permissioned burn, the bond account signing as burn authority), pays price x quantity from the offer's vault (SPL Token TransferChecked, the bond account signing as the vault's authority), and takes them off the holder's record and the supply, saving both histories first.
- 1bond_servicingSellToOffer45,211 CUpayer: BH#2 signs writesbond: Bond account writesmint: Bond token (mint) writesholder: Holder record · BH#2 writesowner: BH#2holder_bonds: BH#2 · bonds writesvault: Vault · offer 0 (buyback) writescash_mint: Cash token (mint)holder_cash: BH#2 · cash writestoken_program: Token-2022cash_token_program: SPL Tokenassociated_token_program: Associated Token Account programsystem_program: System programseller: BH#2 signsoffer: Offer 0 · buyback writes
- PositionsSaved: BH#2 held 5 at record moment 0
- BondsBoughtBack: BH#2, 2 burned, 1,980.00 paid
- 2Token-2022PermissionedBurnExtension2,878 CU
- 2SPL TokentransferChecked105 CU
ConstraintRawthe signer is the holder whose bonds are soldWrongOfferKinda first-come buybackOfferNotFundedthe offer is confirmed fundedOfferNotOpeninside the windowExceedsOfferquantity above zero and the offer's total within its maximumInsufficientUnlockednot more than the holder's untendered bondsPositionMismatchthe holder record agrees with its bond balance
| Account | Before | After | Change |
|---|---|---|---|
| BH#2 · bonds | 5 | 3 | -2 |
| BH#2 · cash | 59,590.00 | 61,570.00 | +1,980.00 |
| Vault · offer 0 (buyback) | 3,960.00 | 1,980.00 | -1,980.00 |
open_event (coupon)
may be sent by: AnyoneAfter a record moment anyone opens its coupon. The program reads the supply at that moment from the bond's supply history, fixes the budget (coupon on the whole supply, rounded down once) and creates the event and its own vault. No holder is counted.
- 1bond_servicingOpenEvent18,896 CUpayer: Registrar signs writesbond: Bond accountevent: Event 0 · coupon writescash_mint: Cash token (mint)vault: Vault · coupon 0 writescash_token_program: SPL Tokensystem_program: System program
- EventOpened: event 0, supply at the record moment 33, budget 1,650.00
- 2System programcreateAccount
- 2System programcreateAccount
- 2SPL TokeninitializeAccount3233 CU
RecordMomentNotReachedthe coupon's record moment has passedaccount existsone event per index, at ["event", bond, index], with its vault at ["vault", event]
No token balance changed: only program accounts were written.
transactionsource: programs/bond_servicing/src/instructions/open_event.rs
the bank pays into the coupon's vault
may be sent by: BankNot a bond_servicing instruction: the bank credits the coupon's vault with the budget (SPL Token MintTo). In real markets, the issuer pays the coupon to the paying agent.
- 1SPL TokenmintTo120 CU
owner does not matchSPL Token: only the cash token's mint authority mints
| Account | Before | After | Change |
|---|---|---|---|
| Vault · coupon 0 | 0.00 | 1,650.00 | +1,650.00 |
confirm_funding
may be sent by: AnyoneAnyone confirms that the vault covers the budget. Payment starts only then, so no holder is paid from an event that cannot pay all of them.
- 1bond_servicingConfirmFunding4,830 CU
- EventFunded: event 0, vault 1,650.00 of budget 1,650.00
AlreadyFundednot confirmed beforeInsufficientFundingthe vault holds at least the budget
No token balance changed: only program accounts were written.
pay_coupon
may be sent by: AnyoneAnyone pays a holder its coupon. The amount comes from the holder's own record (its quantity at the record moment) and the terms; the cash goes from the event's vault to the holder's cash account (created if missing), and the holder's paid bit is set in the same transaction. One transaction can pay several holders, one instruction each.
- 1bond_servicingPayCoupon22,374 CUpayer: Registrar signs writesbond: Bond accountevent: Event 0 · couponholder: Holder record · BH#1 writesowner: BH#1vault: Vault · coupon 0 writescash_mint: Cash token (mint)holder_cash: BH#1 · cash writescash_token_program: SPL Tokenassociated_token_program: Associated Token Account programsystem_program: System program
- CouponPaid: BH#1, coupon 0, on 10 bonds, 500.00
- 2SPL TokentransferChecked105 CU
The same transaction also ran PayCoupon for BH#2, BH#4, BH#5, BH#7; only BH#1's PayCoupon is drawn. Balances are the whole transaction's.
WrongEventKinda coupon eventNotFundedthe event is confirmed fundedPaymentNotOpenbond time has reached the payment timeAlreadyPaidthe holder's paid bit for this coupon is not setNothingToPaythe position at the record moment gives a coupon above zeroConstraintHasOnethe holder record belongs to this wallet and bond; the vault is the event's
| Account | Before | After | Change |
|---|---|---|---|
| BH#1 · cash | 87,650.00 | 88,150.00 | +500.00 |
| Vault · coupon 0 | 1,650.00 | 0.00 | -1,650.00 |
| BH#2 · cash | 61,570.00 | 61,820.00 | +250.00 |
| BH#5 · cash | 26,250.00 | 26,450.00 | +200.00 |
| BH#4 · cash | 27,600.00 | 28,000.00 | +400.00 |
| BH#7 · cash | 0.00 | 300.00 | +300.00 |
change_tender
may be sent by: HolderA holder tenders bonds into the tender offer. The bonds do not move: the holder record locks them, so a coupon whose record moment falls in the window is still paid on them. The tender is numbered in the order it was made.
- 1bond_servicingChangeTender26,527 CUowner: BH#1 signs writesbond: Bond accountoffer: Offer 1 · tender writesholder: Holder record · BH#1 writestender: Tender · BH#1 writessystem_program: System program
- TenderChanged: BH#1 tenders 6; offer total 6
- 2System programcreateAccount
ConstraintHasOnethe signer owns the holder recordWrongOfferKinda pro-rata tender offerOfferNotFundedthe offer is confirmed fundedOfferNotOpeninside the windowExceedsOfferquantity above zero; a withdrawal not more than tenderedInsufficientUnlockednot more than the holder's untendered bonds
No token balance changed: only program accounts were written.
resolve_tender
may be sent by: AnyoneAfter the window anyone resolves the tenders, in the order they were made. If the offer is oversubscribed, each takes the pro-rata share of the running total, rounded down, minus what the earlier ones took, so the offer buys exactly its maximum. The rejected part is unlocked. No cash or token account is involved.
- 1bond_servicingResolveTender16,467 CUbond: Bond accountoffer: Offer 1 · tender writesholder: Holder record · BH#1 writestender: Tender · BH#1 writes
- TenderResolved: BH#1, 3 accepted, 3 released
WrongOfferKinda pro-rata tender offerOfferStillOpenthe window has closedTenderOutOfOrderthis tender's number is the next to resolve
No token balance changed: only program accounts were written.
settle_tender
may be sent by: AnyoneAnyone settles a resolved tender: the accepted bonds are burned and paid for in one transaction, as in the buyback, and released from the lock.
- 1bond_servicingSettleTender52,631 CUpayer: Registrar signs writesbond: Bond account writesmint: Bond token (mint) writesholder: Holder record · BH#1 writesowner: BH#1holder_bonds: BH#1 · bonds writesvault: Vault · offer 1 (tender) writescash_mint: Cash token (mint)holder_cash: BH#1 · cash writestoken_program: Token-2022cash_token_program: SPL Tokenassociated_token_program: Associated Token Account programsystem_program: System programoffer: Offer 1 · tender writestender: Tender · BH#1 writes
- BondsBoughtBack: BH#1, 3 burned, 3,000.00 paid
- 2Token-2022PermissionedBurnExtension2,878 CU
- 2SPL TokentransferChecked105 CU
WrongOfferKinda pro-rata tender offerOfferStillOpenthe window has closedAlreadySettlednot settled beforePositionMismatchthe holder record agrees with its bond balance
| Account | Before | After | Change |
|---|---|---|---|
| Vault · offer 1 (tender) | 6,000.00 | 3,000.00 | -3,000.00 |
| BH#1 · cash | 88,950.00 | 91,950.00 | +3,000.00 |
| BH#1 · bonds | 8 | 5 | -3 |
redeem_holder
may be sent by: AnyoneAt maturity, after the redemption is opened and funded like a coupon, anyone redeems a holder: the program burns its bonds and pays the principal from the redemption vault in one transaction, so no bond is burned unpaid and no principal is paid for a live bond.
- 1bond_servicingRedeemHolder40,611 CUpayer: Registrar signs writesbond: Bond account writesmint: Bond token (mint) writesevent: Event 4 · redemptionholder: Holder record · BH#1 writesowner: BH#1holder_bonds: BH#1 · bonds writesvault: Vault · redemption writescash_mint: Cash token (mint)holder_cash: BH#1 · cash writestoken_program: Token-2022cash_token_program: SPL Tokenassociated_token_program: Associated Token Account programsystem_program: System program
- PositionsSaved: BH#1 held 5 at record moment 3
- BondsRedeemed: BH#1, 5 burned, 5,000.00 principal
- 2Token-2022PermissionedBurnExtension2,878 CU
- 2SPL TokentransferChecked105 CU
The same transaction also ran RedeemHolder for BH#2, BH#3, BH#4; only BH#1's RedeemHolder is drawn. Balances are the whole transaction's.
WrongEventKindthe redemption eventNotFundedthe redemption is confirmed fundedPaymentNotOpenbond time has reached the payment timePositionMismatchthe holder record agrees with its bond balanceNothingToPaythe holder has untendered bonds
| Account | Before | After | Change |
|---|---|---|---|
| BH#2 · bonds | 2 | 0 | -2 |
| BH#3 · bonds | 1 | 0 | -1 |
| BH#3 · cash | 15,800.00 | 16,800.00 | +1,000.00 |
| BH#1 · cash | 92,200.00 | 97,200.00 | +5,000.00 |
| BH#1 · bonds | 5 | 0 | -5 |
| BH#2 · cash | 62,170.00 | 64,170.00 | +2,000.00 |
| BH#4 · cash | 28,200.00 | 32,200.00 | +4,000.00 |
| BH#4 · bonds | 4 | 0 | -4 |
| Vault · redemption | 23,000.00 | 11,000.00 | -12,000.00 |