← Program logic

programs/bond_servicing/src/settlement.rs

Source at revision e5ff2b3cb157, the source of the program deployed on devnet. SHA-256 of this file 23bfc84f6f47dbc2f59bb9a41a57b634cd70657a17b50c97edfb129fb3b3475c

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}