1//! Moving value out of a vault, shared by coupon payment, redemption, buyback and tender settlement.
2//! - `pay_cash`: transfer from a vault (signed by the Bond PDA). If the recipient is a Token-2022 account that
3//! requires incoming memos, a Memo instruction is invoked right before the transfer, as Token-2022 requires.
4//! - `burn_and_pay`: retire bonds against cash in one transaction (Token-2022 permissioned burn, signed by the
5//! Bond PDA as burn authority and permanent delegate, then `pay_cash`).
6//! - `retire_quantity`: save the holder's and the bond's histories, then take the burned quantity off both, so every
7//! right earned at past record moments stays payable.
8
9use anchor_lang::prelude::*;
10use anchor_lang::solana_program::instruction::{AccountMeta, Instruction};
11use anchor_lang::solana_program::program::{invoke, invoke_signed};
12use anchor_spl::token_2022::spl_token_2022::{
13 self,
14 extension::{memo_transfer::memo_required, StateWithExtensions},
15 state::Account as TokenAccountState,
16};
17use anchor_spl::token_interface::{self, TransferChecked};
18
19use crate::constants::*;
20use crate::error::BondError;
21use crate::events::PositionsSaved;
22use crate::state::{Bond, HolderRecord};
23
24/// SPL Memo program (v3).
25pub const MEMO_PROGRAM_ID: Pubkey = pubkey!("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
26
27pub struct Cash<'a, 'info> {
28 pub cash_token_program: &'a AccountInfo<'info>,
29 pub vault: &'a AccountInfo<'info>,
30 pub cash_mint: &'a AccountInfo<'info>,
31 pub holder_cash: &'a AccountInfo<'info>,
32 pub bond: &'a AccountInfo<'info>,
33 pub memo_program: Option<&'a AccountInfo<'info>>,
34}
35
36/// Does the recipient require a memo with every incoming transfer (Token-2022 MemoTransfer extension)?
37fn requires_memo(account: &AccountInfo) -> Result<bool> {
38 if *account.owner != spl_token_2022::ID {
39 return Ok(false);
40 }
41 let data = account.try_borrow_data()?;
42 let state = StateWithExtensions::<TokenAccountState>::unpack(&data).map_err(|_| error!(BondError::UnsupportedCashToken))?;
43 Ok(memo_required(&state))
44}
45
46/// Pay `amount` from the vault to the holder's cash account, signed by the Bond PDA.
47pub fn pay_cash(c: &Cash, amount: u64, decimals: u8, memo: &str, seeds: &[&[u8]]) -> Result<()> {
48 if requires_memo(c.holder_cash)? {
49 let memo_program = c.memo_program.ok_or(BondError::MemoProgramMissing)?;
50 require_keys_eq!(memo_program.key(), MEMO_PROGRAM_ID, BondError::MemoProgramMissing);
51 let ix = Instruction { program_id: MEMO_PROGRAM_ID, accounts: vec![], data: memo.as_bytes().to_vec() };
52 invoke(&ix, &[memo_program.clone()])?;
53 }
54 token_interface::transfer_checked(
55 CpiContext::new_with_signer(
56 c.cash_token_program.key(),
57 TransferChecked {
58 from: c.vault.clone(),
59 mint: c.cash_mint.clone(),
60 to: c.holder_cash.clone(),
61 authority: c.bond.clone(),
62 },
63 &[seeds],
64 ),
65 amount,
66 decimals,
67 )
68}
69
70/// Burn `quantity` bonds (0 decimals) from the holder's bond account, signed by the Bond PDA.
71pub fn burn_bonds<'info>(
72 token_program: &AccountInfo<'info>,
73 holder_bonds: &AccountInfo<'info>,
74 mint: &AccountInfo<'info>,
75 bond: &AccountInfo<'info>,
76 quantity: u64,
77 seeds: &[&[u8]],
78) -> Result<()> {
79 let mut data = vec![PERMISSIONED_BURN_EXTENSION, PERMISSIONED_BURN_BURN_CHECKED];
80 data.extend_from_slice(&quantity.to_le_bytes());
81 data.push(0);
82 let burn = Instruction {
83 program_id: token_program.key(),
84 accounts: vec![
85 AccountMeta::new(holder_bonds.key(), false),
86 AccountMeta::new(mint.key(), false),
87 AccountMeta::new_readonly(bond.key(), true),
88 AccountMeta::new_readonly(bond.key(), true),
89 ],
90 data,
91 };
92 invoke_signed(&burn, &[holder_bonds.clone(), mint.clone(), bond.clone(), bond.clone(), token_program.clone()], &[seeds])?;
93 Ok(())
94}
95
96/// Save the holder's and the bond's histories, then take `quantity` off both.
97pub fn retire_quantity(holder: &mut HolderRecord, bond: &mut Bond, bond_key: Pubkey, passed: u16, quantity: u64) -> Result<()> {
98 let before = holder.position.quantity;
99 if let Some((from_event, to_event)) = holder.position.save(passed) {
100 emit!(PositionsSaved { bond: bond_key, owner: holder.owner, from_event, to_event, quantity: before });
101 }
102 holder.position.quantity = before.checked_sub(quantity).ok_or(BondError::Overflow)?;
103 bond.supply.save(passed);
104 bond.supply.quantity = bond.supply.quantity.checked_sub(quantity).ok_or(BondError::Overflow)?;
105 Ok(())
106}