Bond servicing on Solana · measured on devnet
It pays what it should. It refuses what it shouldn't.
One Solana program services a tokenized bond: coupons, redemption, buyback and tender. Pick a party: what it expects, kept on devnet, and what was tried against it, refused. Or see how it works.
Holder
investorExpectsPaid exactly, on time, for what it held
-
Paid for the bonds held at the record moment4,000 of 4,000coupon payments exact, 1,000 holders; paying by holdings at payment time would have got 1,039 wrong
-
Sells right after the record moment, keeps the couponBH#458: 6 ssold 3 of 12 bonds 6 s after the record moment; paid on 12: 600.00
-
The listing's exampleBH#1: 500.0010 bonds x 1,000 x 10% / 2
Refused Attacker moves BH#1's bonds to itself only the owner moves its bonds · transaction
Would cost: BH#1 loses its bonds. Logged:owner does not matchRefused Attacker moves BH#1's bonds naming the program, the permanent delegate only the program acts as permanent delegate · transaction
Would cost: BH#1 loses its bonds. Logged:MissingRequiredSignatureRefused Attacker burns BH#1's bonds standard burns are disabled for this bond · transaction
Would cost: BH#1 loses coupons and principal. Logged:Invalid instructionRefused Attacker burns BH#1's bonds with the permissioned burn, naming itself burn authority only the program is burn authority · transaction
Would cost: BH#1 loses coupons and principal. Logged:InvalidAccountDataRefused Attacker burns BH#1's bonds with the permissioned burn, naming the program, the burn authority only the program signs as burn authority · transaction
Would cost: BH#1 loses coupons and principal. Logged:MissingRequiredSignatureRefused Attacker freezes BH#1's bond account the bond has no freeze authority · transaction
Would cost: BH#1 cannot sell. Logged:This token mint cannot freeze accountsRefused Attacker pays BH#1's coupon into its own cash account cash goes only to the holder's own account · transaction
Would cost: BH#1's coupon. Logged:ConstraintTokenOwner: A token owner constraint was violated.Refused Attacker presents BH#1's holder record as its own a holder record belongs to one wallet · transaction
Would cost: BH#1's coupon. Logged:ConstraintSeeds: A seeds constraint was violated.Refused Attacker redeems BH#1's bonds into its own cash account principal goes only to the holder's own account · transaction
Would cost: BH#1's principal. Logged:ConstraintTokenOwner: A token owner constraint was violated.Refused Attacker sells BH#1's bonds to the buyback only the holder sells its bonds · transaction
Would cost: BH#1's bonds sold without its consent. Logged:ConstraintRaw: A raw constraint was violated.
HowA transfer first saves both holders' positions at every record moment passed. The coupon is computed from that saved position and paid only to the holder's own cash account.
Issuer
borrowerExpectsPays what the terms say, not a unit more
-
Coupons paid = the terms2,008,350.004 coupons to 1,000 holders; every vault: inflow = payments + leftover
-
Bonds retired only against cash948 of 948holders' buyback, tender and redemption: bonds burned and cash paid in one transaction
-
Its only task: deposit the cash7 of 2,193transactions in the 1000-holder run were the issuer's
Refused Attacker mints 1,000 bonds to itself only the program mints bonds · transaction
Would cost: the issuer owes coupons and principal on bonds it never sold. Logged:owner does not matchRefused Attacker mints 1,000 bonds naming the program as mint authority only the program signs as the program · transaction
Would cost: the issuer owes coupons and principal on bonds it never sold. Logged:MissingRequiredSignatureRefused Registrar places 100 new bonds with BH#2 the issue closes at the first record moment · transaction
Would cost: bonds issued after the issue closed. Logged:PlacementClosed: Placement is allowed only before the first record moment.Refused Attacker pays BH#1's coupon again one payment per holder per coupon · transaction
Would cost: the issuer pays a coupon twice; the last holders find the vault empty. Logged:AlreadyPaid: Already paid.Refused BH#3 bought after the record moment and asks for the coupon the coupon follows the record moment · transaction
Would cost: the seller's coupon paid again to the buyer. Logged:NothingToPay: Nothing to pay.Refused Attacker redeems BH#1 again redeemed bonds are burned · transaction
Would cost: principal paid twice. Logged:NothingToPay: Nothing to pay.Refused Attacker pays BH#1's coupon 1 from coupon 0's vault each payment has its own vault · transaction
Would cost: one coupon's cash spent on another. Logged:ConstraintHasOne: A has one constraint was violated.Refused Attacker pays BH#1 a coupon from the redemption vault principal cash pays only principal · transaction
Would cost: principal cash paid out as coupons. Logged:WrongEventKind: Instruction does not apply to this event type.Refused BH#1 sells 1 bond to the buyback after its maximum is reached an offer buys up to its maximum · transaction
Would cost: the issuer buys more bonds than it offered. Logged:ExceedsOffer: Quantity exceeds what the offer still accepts.Refused BH#1 transfers all its bonds to BH#2, including those tendered to the issuer tendered bonds are locked · transaction
Would cost: the same bonds sold twice: to the issuer and to a buyer. Logged:BondsLocked: Bonds are locked in a tender.Refused Attacker moves the vault's cash to itself a vault pays only through the program · transaction
Would cost: the coupon's cash is gone. Logged:owner does not matchRefused Attacker moves the vault's cash to itself naming the program, the vault's owner only the program signs for a vault · transaction
Would cost: the coupon's cash is gone. Logged:MissingRequiredSignature
HowEach coupon and the redemption has its own vault, funded with exactly its budget. A payment and the holder's paid flag are one transaction.
Depository
registrar and paying agentExpectsA correct register, and no new problems
-
The register at every record moment, trading open1,000 holders600 trades 2 s to 22 min after record moments; cost per holder paid: 833 to 2,500 lamports
-
Works when the operator stops12 of 12our operator stopped after coupon 0; a wallet with no role paid the rest and redeemed everyone
-
Anyone can service; nobody can redirect0 to the attackerthe attacker's wallet paid coupons and principal in 4 transactions: all cash went to holders
Refused Attacker registers itself as a holder, signing as registrar only the registrar registers holders · transaction
Would cost: an unvetted wallet in the register. Logged:ConstraintHasOne: A has one constraint was violated.Refused BH#1 sends a bond to an unregistered wallet (the attacker) bonds go only to registered holders · transaction
Would cost: bonds held outside the register. Logged:AccountOwnedByWrongProgram: The given account is owned by a different program than expected.Refused BH#1 moves a bond to a second account of its own one bond account per holder · transaction
Would cost: a position the register does not see. Logged:NotAssociatedTokenAccount: Bonds may be held only in the owner's associated token account.Refused BH#1 sends a bond to BH#2 with the old transfer instruction, which skips the transfer hook every transfer must pass the program · transaction
Would cost: a bond moves without its record-moment position being saved. Logged:custom program error: 0x1fRefused Attacker calls the transfer hook directly to add 5 bonds to BH#2's record the program's hook runs only inside a real transfer · transaction
Would cost: coupons paid on bonds nobody holds. Logged:NotTransferring: The hook was called outside a token transfer.Refused Attacker confirms the coupon funded funding must cover the whole budget · transaction
Would cost: the last holders find the vault empty. Logged:InsufficientFunding: Vault balance does not cover the event's budget.Refused Attacker pays BH#1 from the unconfirmed vault no payment before full funding · transaction
Would cost: the last holders find the vault empty. Logged:NotFunded: The event is not funded.Refused Attacker makes itself the program's upgrade authority only the upgrade authority changes the program · transaction
Would cost: a replaced program could empty every vault. Logged:Incorrect authority provided
HowThe program is the register and the paying agent. Opening, funding confirmation, payment and redemption can be sent by any wallet; none can change who is paid or how much.
Bank
cash railExpectsIts money and its controls respected
-
Cash enters a vault only from the bank4 of 4vault checks: deposits only from the bank's mints, payments only to holders
-
Its freeze holds; the bond keeps payingBH#5 waitsthe bank froze BH#5's cash account for coupon 1: the others were paid, BH#5 after the thaw
Refused Registrar creates a bond paid in a cash token that keeps a 1% fee on every transfer cash tokens that change amounts are refused · transaction
Would cost: holders receive 1% less than the terms. Logged:UnsupportedCashToken: Cash token has an extension that changes or conditions delivery (transfer fee, transfer hook, non-transferable) or one this program cannot read.Refused Anyone pays BH#5, whose cash account the bank froze the bank's freeze holds; the others were paid first · transaction
Would cost: nothing: the others are already paid, and the right stays open. Logged:Account is frozen
HowThe program moves cash only with the cash token's own transfer: it cannot pass a frozen account, and it refuses at bond creation any cash token that changes amounts.
Exchange
trading venueExpectsTrading open every day until maturity
-
Open through every record moment600 trades2 s to 22 min after record moments, 1,000 holders
-
Nobody can freeze a holderNo freeze authoritythe bond token was created without one
Refused BH#2 sells 1 bond to BH#1 trading closes at the final record moment · transaction
Would cost: a bond changes hands while its principal is being paid. Logged:TransfersClosedAtMaturity: Transfers are closed from the final record moment (redemption).
HowNo snapshot and no freeze: the hook saves positions as bonds move, so a record moment needs no pause.
Auditor
or regulatorExpectsEvery payment checkable from public data
-
Every payment recomputed from the ledger3,959 of 3,959checks over 4,085 transactions; 1,000 of 1,000 holders' cash equals the terms to the unit
-
Tampering is caught2 checks failafter 0.000001 is sent into a vault from outside (local network)
-
The attacker run, audited the same way35 of 35checks pass; 33 of 33 attacks refused, each a failed transaction anyone can open
HowThe audit reads only the public ledger: no program accounts, no operator report. It recomputes every amount from the bond's terms.
How it works
Every bond needs the same processes. The program runs them under real constraints; where it has to differ, the outcome must not.The issuer sells bonds on fixed terms. Each buyer holds what it bought, and the supply is fixed.
create_bond writes the terms and creates the bond token with the program as its only mint and burn authority and no freeze authority. register_holder admits a wallet; place mints bonds to it, only before the first record moment.
Anyone can open a token account. A bond needs known holders and a supply nobody can inflate.
Only the program changes the supply, and it records every change, so the supply at any record moment is known without counting holders.
- Sent by
- registrar
- Reads
- terms
- Writes
- bond, holder records
- Moves
- bonds minted to holders
- 10,251 bondsplaced with 1,000 registered holders
Refused Attacker registers itself as a holder, signing as registrar only the registrar registers holders · transaction
Would cost: an unvetted wallet in the register. Logged:ConstraintHasOne: A has one constraint was violated.Refused Attacker mints 1,000 bonds to itself only the program mints bonds · transaction
Would cost: the issuer owes coupons and principal on bonds it never sold. Logged:owner does not matchRefused Attacker mints 1,000 bonds naming the program as mint authority only the program signs as the program · transaction
Would cost: the issuer owes coupons and principal on bonds it never sold. Logged:MissingRequiredSignatureRefused Registrar places 100 new bonds with BH#2 the issue closes at the first record moment · transaction
Would cost: bonds issued after the issue closed. Logged:PlacementClosed: Placement is allowed only before the first record moment.Refused Registrar creates a bond paid in a cash token that keeps a 1% fee on every transfer cash tokens that change amounts are refused · transaction
Would cost: holders receive 1% less than the terms. Logged:UnsupportedCashToken: Cash token has an extension that changes or conditions delivery (transfer fee, transfer hook, non-transferable) or one this program cannot read.
A trade changes ownership at once. The register always says who holds what.
Every transfer runs the program's transfer hook in the same transaction. Both sides must be registered; the hook saves their holdings at the record moments passed, then updates both holder records.
Transfers run in the token program, not in ours, and one holder could open several accounts.
If the hook fails, the transfer fails. One account per holder, and a transfer is refused if a holder record ever disagrees with its balance.
- Sent by
- holder; the token program calls the hook
- Reads
- bond time
- Writes
- both holder records
- Moves
- bonds, seller to buyer
- 600 tradesbetween 1,000 holders, every one through the hook
Refused Attacker moves BH#1's bonds to itself only the owner moves its bonds · transaction
Would cost: BH#1 loses its bonds. Logged:owner does not matchRefused Attacker moves BH#1's bonds naming the program, the permanent delegate only the program acts as permanent delegate · transaction
Would cost: BH#1 loses its bonds. Logged:MissingRequiredSignatureRefused Attacker freezes BH#1's bond account the bond has no freeze authority · transaction
Would cost: BH#1 cannot sell. Logged:This token mint cannot freeze accountsRefused BH#1 sends a bond to an unregistered wallet (the attacker) bonds go only to registered holders · transaction
Would cost: bonds held outside the register. Logged:AccountOwnedByWrongProgram: The given account is owned by a different program than expected.Refused BH#1 moves a bond to a second account of its own one bond account per holder · transaction
Would cost: a position the register does not see. Logged:NotAssociatedTokenAccount: Bonds may be held only in the owner's associated token account.Refused BH#1 sends a bond to BH#2 with the old transfer instruction, which skips the transfer hook every transfer must pass the program · transaction
Would cost: a bond moves without its record-moment position being saved. Logged:custom program error: 0x1fRefused BH#1 transfers all its bonds to BH#2, including those tendered to the issuer tendered bonds are locked · transaction
Would cost: the same bonds sold twice: to the issuer and to a buyer. Logged:BondsLocked: Bonds are locked in a tender.Refused BH#2 sells 1 bond to BH#1 trading closes at the final record moment · transaction
Would cost: a bond changes hands while its principal is being paid. Logged:TransfersClosedAtMaturity: Transfers are closed from the final record moment (redemption).
At the record moment the register is fixed: each holder is owed for what it holds at that instant.
Nothing runs at the record moment. The first change to a holding after it saves the holding it replaces; a holding that never changed is still the one held then.
A program sees only the accounts in a transaction and runs only when one is sent: it cannot read every holder at 09:00, and nothing is sent at 09:00 by itself.
Between two changes a holding is constant, so what the next change saves is exactly the holding at the record moment, however late anyone looks. No freeze, no deadline.
- Sent by
- nobody: it happens inside the next transfer
- Reads
- bond time, record moments
- Writes
- holding history, supply history
- Moves
- nothing
- 4,000 of 4,000payments on the saved holdings equal the independent audit's register at the record moments
- 600 trades2 s to 22 min after record moments; paying by holdings at payment time: 1,039 wrong
Refused Attacker calls the transfer hook directly to add 5 bonds to BH#2's record the program's hook runs only inside a real transfer · transaction
Would cost: coupons paid on bonds nobody holds. Logged:NotTransferring: The hook was called outside a token transfer.
Owed = holding at the record moment x face x rate / periods, as the terms say.
Computed inside each payment from the saved holding and the terms, in integers, rounded down once per holder. The event's budget is the same formula on the supply at the record moment.
Token amounts are whole smallest units; there are no fractions on chain.
Rounding once on each holder's total keeps the sum within the budget; any residue stays visible in the event's vault.
- Sent by
- part of each payment
- Reads
- terms, saved holding
- Writes
- nothing
- Moves
- nothing
- BH#1: 500.0010 bonds x 1,000 x 10% / 2, the listing's example
- passedevery amount checked against QuantLib and FinancePy
Refused BH#3 bought after the record moment and asks for the coupon the coupon follows the record moment · transaction
Would cost: the seller's coupon paid again to the buyer. Logged:NothingToPay: Nothing to pay.
The issuer provides the full amount before payment.
After the record moment anyone opens the event: it gets its own vault and a budget fixed by the supply at the record moment. The issuer deposits; anyone confirms, accepted only if the vault covers the budget.
Cash is a separate token that the issuer moves outside the program, at its own pace.
No holder is paid from an event that cannot pay all, and one event's cash cannot pay another. The issuer's only action is the deposit.
- Sent by
- anyone (open, confirm); issuer (deposit)
- Reads
- supply history, vault balance
- Writes
- event, its vault
- Moves
- cash, issuer to the event's vault
- 7 of 2,193transactions were the issuer's deposits
Refused Attacker confirms the coupon funded funding must cover the whole budget · transaction
Would cost: the last holders find the vault empty. Logged:InsufficientFunding: Vault balance does not cover the event's budget.Refused Attacker pays BH#1 from the unconfirmed vault no payment before full funding · transaction
Would cost: the last holders find the vault empty. Logged:NotFunded: The event is not funded.Refused Attacker moves the vault's cash to itself a vault pays only through the program · transaction
Would cost: the coupon's cash is gone. Logged:owner does not matchRefused Attacker moves the vault's cash to itself naming the program, the vault's owner only the program signs for a vault · transaction
Would cost: the coupon's cash is gone. Logged:MissingRequiredSignature
On the payment date each entitled holder receives its amount, exactly once.
Any wallet, any time after payment opens, in any order, pays one holder: cash from the event's vault to the holder's own cash account and the holder's paid flag, in one transaction.
Paying 1,000 holders takes many transactions; someone must send them; the operator may stop; a holder's cash account may be frozen.
Whoever sends it and whenever, state fixes the amount and the recipient; the paid flag makes it once; rights never expire; one holder's failure blocks nobody.
- Sent by
- anyone
- Reads
- bond, event, holder record
- Writes
- holder's paid flag
- Moves
- cash, event vault to the holder's own account
- 2,008,350.004 coupons to 1,000 holders, exact
- 12 of 12operator stopped after coupon 0; a wallet with no role paid the rest
- 0 to itselfthe attacker's wallet paid coupons and principal in 4 transactions
- BH#5 waitedits cash account frozen by the bank: the others were paid first, BH#5 after the thaw
Refused Attacker pays BH#1's coupon into its own cash account cash goes only to the holder's own account · transaction
Would cost: BH#1's coupon. Logged:ConstraintTokenOwner: A token owner constraint was violated.Refused Attacker presents BH#1's holder record as its own a holder record belongs to one wallet · transaction
Would cost: BH#1's coupon. Logged:ConstraintSeeds: A seeds constraint was violated.Refused Attacker pays BH#1's coupon again one payment per holder per coupon · transaction
Would cost: the issuer pays a coupon twice; the last holders find the vault empty. Logged:AlreadyPaid: Already paid.Refused Attacker pays BH#1's coupon 1 from coupon 0's vault each payment has its own vault · transaction
Would cost: one coupon's cash spent on another. Logged:ConstraintHasOne: A has one constraint was violated.Refused Attacker pays BH#1 a coupon from the redemption vault principal cash pays only principal · transaction
Would cost: principal cash paid out as coupons. Logged:WrongEventKind: Instruction does not apply to this event type.Refused Anyone pays BH#5, whose cash account the bank froze the bank's freeze holds; the others were paid first · transaction
Would cost: nothing: the others are already paid, and the right stays open. Logged:Account is frozen
At maturity holders receive the principal and the bonds cease to exist.
Transfers close at the final record moment. Any wallet redeems a holder: its bonds are burned and the principal paid in one transaction.
Redemption takes one transaction per few holders, and bonds tendered to an open offer are not free.
Burn and payment are one transaction: no bond is burned unpaid and no principal is paid for a live bond. Bonds committed to an offer are left to it.
- Sent by
- anyone
- Reads
- bond, event, holder record
- Writes
- holder record, supply
- Moves
- bonds burned; cash, vault to holder
- 9,766 bondsredeemed from 934 holders; final supply 0
Refused Attacker redeems BH#1's bonds into its own cash account principal goes only to the holder's own account · transaction
Would cost: BH#1's principal. Logged:ConstraintTokenOwner: A token owner constraint was violated.Refused Attacker redeems BH#1 again redeemed bonds are burned · transaction
Would cost: principal paid twice. Logged:NothingToPay: Nothing to pay.Refused Attacker burns BH#1's bonds standard burns are disabled for this bond · transaction
Would cost: BH#1 loses coupons and principal. Logged:Invalid instructionRefused Attacker burns BH#1's bonds with the permissioned burn, naming itself burn authority only the program is burn authority · transaction
Would cost: BH#1 loses coupons and principal. Logged:InvalidAccountDataRefused Attacker burns BH#1's bonds with the permissioned burn, naming the program, the burn authority only the program signs as burn authority · transaction
Would cost: BH#1 loses coupons and principal. Logged:MissingRequiredSignature
The issuer buys bonds back at a price: first come up to a maximum, or pro rata among tenders. Bought bonds cease to exist.
The registrar opens an offer with its own funded vault. A sale burns and pays at once. A tender locks bonds in the holder's own account; after the window anyone settles each tender: accepted = tendered x maximum / total, rounded down.
The tender window spans a record moment, and bonds come in whole units.
Tendered bonds never move, so they keep the coupon whose record moment falls in the window. Rounding down never buys more than the maximum; it may buy fewer, and the unspent cash stays in the offer's vault.
- Sent by
- registrar (offer); holder (sell, tender); anyone (settle)
- Reads
- offer, holder record
- Writes
- offer, tender, holder record, supply
- Moves
- bonds burned; cash, offer vault to holder
- 176 bondsbought back first come, maximum 410
- 309 bondsaccepted pro rata from tenders, maximum 410
- 132 of 132tendering holders paid coupon 2 on their tendered bonds
Refused Attacker sells BH#1's bonds to the buyback only the holder sells its bonds · transaction
Would cost: BH#1's bonds sold without its consent. Logged:ConstraintRaw: A raw constraint was violated.Refused BH#1 sells 1 bond to the buyback after its maximum is reached an offer buys up to its maximum · transaction
Would cost: the issuer buys more bonds than it offered. Logged:ExceedsOffer: Quantity exceeds what the offer still accepts.
Anyone can reconcile the register and every payment.
Every change is a ledger transaction. An independent audit reads only the public ledger and recomputes every amount from the terms.
A ledger shows transactions, not obligations: the auditor must rebuild the obligations itself.
The audit needs no operator report and no program accounts, and it fails on one unit out of place.
- Sent by
- anyone, off chain
- Reads
- public ledger, terms
- Writes
- nothing
- Moves
- nothing
- 3,959 of 3,959checks over 4,085 transactions
- 35 of 35checks on the attacker run
- 2 checks failafter 0.000001 is sent into a vault from outside (local network)
The design that follows
- Record on write, not by snapshotHoldings at record moments are saved by the next change, so the market never stops and nothing has a deadline.
- Every balance change passes the programTransfer hook, the program as only mint and burn authority, no freeze authority; records checked against balances.
- Outcomes follow from state, so anyone may actOpening, confirming, paying, redeeming and settling need no authority: the sender cannot change who gets what.
- Each obligation isolatedOne vault per event and offer, one paid flag per holder per coupon; a payment writes only that holder's record and that vault.
- Value moves only with its counterpartCash and paid flag in one transaction; burn and payment in one transaction.
- Checked from outsideAn audit from the public ledger alone, failing on one unit out of place.
| Process | Sent by | Bond: terms, supply | Holder record | Event | Offer, tender | Vault (cash) | Bond token |
|---|---|---|---|---|---|---|---|
create_bond | registrar | create | create | ||||
register_holder | registrar | read | create | ||||
place | registrar | supply | write | mint | |||
transfer + hook | holder | read | write both | move | |||
open_event | anyone | read | create | create | |||
deposit | issuer | cash in | |||||
confirm_funding | anyone | write | read | ||||
pay_coupon | anyone | read | paid flag | read | cash out | ||
redeem_holder | anyone | supply | write | read | cash out | burn | |
create_offer | registrar | read | create | create | |||
sell_to_offer | holder | supply | write | write | cash out | burn | |
change_tender | holder | read | lock | write | |||
settle_tender | anyone | supply | write | read | cash out | burn |
Outcome of the 1000-holder run
Bonds10,251 placed = 176 bought back + 309 tendered + 9,766 redeemed ; 0 left
Cash12,590,250.00 deposited = 12,257,590.00 to holders + 332,660.00 unspent offer cash, still in its vaults ; 0.00 left in coupon and redemption vaults
Assumed, not proven here
- Time is the chain's clock; devnet runs compress a bond day to 1-20 seconds.
- The registrar registers only vetted wallets (investor onboarding is simulated).
- The program's upgrade authority is one wallet: an attacker cannot take it (refused), but its holder could replace the program.
- Cash is a test token minted by a test bank; holders hold directly, with no custodians.