1//! Redeem one holder at maturity. Anyone may call it once the redemption is funded and payment has opened.
2//! In one transaction the program burns the holder's free bonds (permissioned burn: the Bond PDA signs as burn
3//! authority and permanent delegate) and pays their principal from the redemption vault. No bond is burned unpaid,
4//! no principal is paid for a live bond.
5//! - Bonds still committed to an offer (accepted, not yet bought) are left to that offer; the rest is redeemed
6//! now, so a blocked offer cannot hold back the holder's redemption.
7//! - Principal is computed on the cumulative quantity redeemed: principal(redeemed + now) - principal(redeemed),
8//! so partial redemptions round exactly like one.
9//! - The holder's history is saved first, so the final coupon stays payable whether it is paid before or after.
10
11use anchor_lang::prelude::*;
12use anchor_spl::associated_token::AssociatedToken;
13use anchor_spl::token_2022::Token2022;
14use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
15
16use crate::constants::*;
17use crate::error::BondError;
18use crate::events::BondsRedeemed;
19use crate::settlement::{burn_bonds, pay_cash, retire_quantity, Cash};
20use crate::state::{Bond, Event, EventKind, HolderRecord};
21
22#[derive(Accounts)]
23pub struct RedeemHolder<'info> {
24 #[account(mut)]
25 pub payer: Signer<'info>,
26
27 #[account(mut, has_one = cash_mint, has_one = mint)]
28 pub bond: Box<Account<'info, Bond>>,
29
30 #[account(mut)]
31 pub mint: Box<InterfaceAccount<'info, Mint>>,
32
33 #[account(has_one = bond, has_one = vault, seeds = [EVENT_SEED, bond.key().as_ref(), &event.index.to_le_bytes()], bump = event.bump)]
34 pub event: Box<Account<'info, Event>>,
35
36 #[account(mut, has_one = bond, has_one = owner, seeds = [HOLDER_SEED, mint.key().as_ref(), owner.key().as_ref()], bump = holder.bump)]
37 pub holder: Box<Account<'info, HolderRecord>>,
38
39 /// CHECK: the holder's wallet, checked by `has_one = owner` on the holder record.
40 pub owner: UncheckedAccount<'info>,
41
42 #[account(
43 mut,
44 associated_token::mint = mint,
45 associated_token::authority = owner,
46 associated_token::token_program = token_program,
47 )]
48 pub holder_bonds: Box<InterfaceAccount<'info, TokenAccount>>,
49
50 #[account(mut)]
51 pub vault: Box<InterfaceAccount<'info, TokenAccount>>,
52
53 pub cash_mint: Box<InterfaceAccount<'info, Mint>>,
54
55 #[account(
56 init_if_needed,
57 payer = payer,
58 associated_token::mint = cash_mint,
59 associated_token::authority = owner,
60 associated_token::token_program = cash_token_program,
61 )]
62 pub holder_cash: Box<InterfaceAccount<'info, TokenAccount>>,
63
64 /// CHECK: SPL Memo program; needed only when the holder's cash account requires memos (checked when used).
65 pub memo_program: Option<UncheckedAccount<'info>>,
66
67 pub token_program: Program<'info, Token2022>,
68 pub cash_token_program: Interface<'info, TokenInterface>,
69 pub associated_token_program: Program<'info, AssociatedToken>,
70 pub system_program: Program<'info, System>,
71}
72
73pub fn handle_redeem_holder(ctx: Context<RedeemHolder>) -> Result<()> {
74 let a = &mut *ctx.accounts;
75 require!(a.event.kind == EventKind::Redemption, BondError::WrongEventKind);
76 require!(a.event.funded, BondError::NotFunded);
77 require!(a.bond.now()? >= a.event.payment_time, BondError::PaymentNotOpen);
78 let passed = a.bond.periods();
79 let position = a.holder.position.quantity;
80 require!(a.holder_bonds.amount == position, BondError::PositionMismatch);
81 let free = position.checked_sub(a.holder.locked).ok_or(BondError::Overflow)?;
82 require!(free > 0, BondError::NothingToPay);
83 let before = a.holder.redeemed;
84 let after = before.checked_add(free).ok_or(BondError::Overflow)?;
85 let principal = a.bond.principal(after)? - a.bond.principal(before)?;
86
87 let mint_key = a.bond.mint;
88 let bond_key = a.bond.key();
89 let bump = a.bond.bump;
90 let seeds: &[&[u8]] = &[BOND_SEED, mint_key.as_ref(), &[bump]];
91 let bond_info = a.bond.to_account_info();
92 burn_bonds(
93 &a.token_program.to_account_info(),
94 &a.holder_bonds.to_account_info(),
95 &a.mint.to_account_info(),
96 &bond_info,
97 free,
98 seeds,
99 )?;
100 let memo_info = a.memo_program.as_ref().map(|m| m.to_account_info());
101 pay_cash(
102 &Cash {
103 cash_token_program: &a.cash_token_program.to_account_info(),
104 vault: &a.vault.to_account_info(),
105 cash_mint: &a.cash_mint.to_account_info(),
106 holder_cash: &a.holder_cash.to_account_info(),
107 bond: &bond_info,
108 memo_program: memo_info.as_ref(),
109 },
110 principal,
111 a.cash_mint.decimals,
112 "bond_servicing: redemption",
113 seeds,
114 )?;
115 retire_quantity(&mut a.holder, &mut a.bond, bond_key, passed, free)?;
116 a.holder.redeemed = after;
117 emit!(BondsRedeemed { bond: bond_key, owner: a.holder.owner, burned: free, principal });
118 Ok(())
119}