1//! Pay one holder one coupon. Anyone may call it, at any time after payment opens, in any order: the
2//! position comes from the holder's own history (its quantity at the record moment), the amount from the terms.
3//! Writes only the holder's record (the paid bit), the event's vault and the holder's cash account; the bond
4//! and the event are read-only, so payments do not conflict with trading.
5//! The cash transfer and the paid bit are one atomic transaction. The holder's cash account is created if
6//! missing; the holder's bond account is not needed, so a closed bond account cannot block the payment.
7
8use anchor_lang::prelude::*;
9use anchor_spl::associated_token::AssociatedToken;
10use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
11
12use crate::constants::*;
13use crate::error::BondError;
14use crate::events::CouponPaid;
15use crate::settlement::{pay_cash, Cash};
16use crate::state::{Bond, Event, EventKind, HolderRecord};
17
18#[derive(Accounts)]
19pub struct PayCoupon<'info> {
20 #[account(mut)]
21 pub payer: Signer<'info>,
22
23 #[account(has_one = cash_mint)]
24 pub bond: Box<Account<'info, Bond>>,
25
26 #[account(has_one = bond, has_one = vault, seeds = [EVENT_SEED, bond.key().as_ref(), &event.index.to_le_bytes()], bump = event.bump)]
27 pub event: Box<Account<'info, Event>>,
28
29 #[account(mut, has_one = bond, has_one = owner, seeds = [HOLDER_SEED, bond.mint.as_ref(), owner.key().as_ref()], bump = holder.bump)]
30 pub holder: Box<Account<'info, HolderRecord>>,
31
32 /// CHECK: the holder's wallet, checked by `has_one = owner` on the holder record.
33 pub owner: UncheckedAccount<'info>,
34
35 #[account(mut)]
36 pub vault: Box<InterfaceAccount<'info, TokenAccount>>,
37
38 pub cash_mint: Box<InterfaceAccount<'info, Mint>>,
39
40 #[account(
41 init_if_needed,
42 payer = payer,
43 associated_token::mint = cash_mint,
44 associated_token::authority = owner,
45 associated_token::token_program = cash_token_program,
46 )]
47 pub holder_cash: Box<InterfaceAccount<'info, TokenAccount>>,
48
49 /// CHECK: SPL Memo program; needed only when the holder's cash account requires memos (checked when used).
50 pub memo_program: Option<UncheckedAccount<'info>>,
51
52 pub cash_token_program: Interface<'info, TokenInterface>,
53 pub associated_token_program: Program<'info, AssociatedToken>,
54 pub system_program: Program<'info, System>,
55}
56
57pub fn handle_pay_coupon(ctx: Context<PayCoupon>) -> Result<()> {
58 let bond = &ctx.accounts.bond;
59 let event = &ctx.accounts.event;
60 require!(event.kind == EventKind::Coupon, BondError::WrongEventKind);
61 require!(event.funded, BondError::NotFunded);
62 require!(bond.now()? >= event.payment_time, BondError::PaymentNotOpen);
63 let e = event.index;
64 require!(!ctx.accounts.holder.is_paid(e), BondError::AlreadyPaid);
65 let position = ctx.accounts.holder.position.at_moment(e);
66 let amount = bond.coupon(position, e)?;
67 require!(amount > 0, BondError::NothingToPay);
68
69 let mint_key = bond.mint;
70 let seeds: &[&[u8]] = &[BOND_SEED, mint_key.as_ref(), &[bond.bump]];
71 let memo_info = ctx.accounts.memo_program.as_ref().map(|m| m.to_account_info());
72 pay_cash(
73 &Cash {
74 cash_token_program: &ctx.accounts.cash_token_program.to_account_info(),
75 vault: &ctx.accounts.vault.to_account_info(),
76 cash_mint: &ctx.accounts.cash_mint.to_account_info(),
77 holder_cash: &ctx.accounts.holder_cash.to_account_info(),
78 bond: &ctx.accounts.bond.to_account_info(),
79 memo_program: memo_info.as_ref(),
80 },
81 amount,
82 ctx.accounts.cash_mint.decimals,
83 &format!("bond_servicing: coupon {e}"),
84 seeds,
85 )?;
86
87 let holder = &mut ctx.accounts.holder;
88 holder.paid |= 1u64 << e;
89 emit!(CouponPaid { bond: ctx.accounts.bond.key(), event: e, owner: holder.owner, position, amount });
90 Ok(())
91}