← Program logic

programs/bond_servicing/src/instructions/transfer_hook.rs

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

1//! Token-2022 calls this on every bond transfer, after both balances are updated (token-2022 `processor.rs`,
2//! `process_transfer`). It
3//!   - closes trading from the final record moment (balances are frozen for redemption);
4//!   - keeps bonds in associated token accounts only, so one holder record = one position;
5//!   - before moving the two holders' quantities, keeps each holder's quantity for every record moment passed
6//!     since its last change (PositionHistory::save), so no record-moment position is ever lost.
7//! It writes only the two holders' records: transfers between different holders run in parallel.
8
9use std::cell::RefMut;
10
11use anchor_lang::prelude::*;
12use anchor_spl::associated_token::get_associated_token_address_with_program_id;
13use anchor_spl::token_2022::spl_token_2022::{
14    extension::{transfer_hook::TransferHookAccount, BaseStateWithExtensionsMut, PodStateWithExtensionsMut},
15    pod::PodAccount,
16};
17use anchor_spl::token_interface::{Mint, TokenAccount};
18
19use crate::constants::*;
20use crate::error::BondError;
21use crate::events::PositionsSaved;
22use crate::state::{Bond, HolderRecord};
23
24#[derive(Accounts)]
25pub struct TransferHook<'info> {
26    #[account(token::mint = mint)]
27    pub source_token: InterfaceAccount<'info, TokenAccount>,
28
29    pub mint: Box<InterfaceAccount<'info, Mint>>,
30
31    #[account(token::mint = mint)]
32    pub destination_token: InterfaceAccount<'info, TokenAccount>,
33
34    /// CHECK: owner or delegate that authorised the transfer; Token-2022 has already checked it.
35    pub authority: UncheckedAccount<'info>,
36
37    /// CHECK: ExtraAccountMetaList at the interface seeds.
38    #[account(seeds = [EXTRA_ACCOUNT_METAS_SEED, mint.key().as_ref()], bump)]
39    pub extra_account_meta_list: UncheckedAccount<'info>,
40
41    #[account(seeds = [BOND_SEED, mint.key().as_ref()], bump = bond.bump)]
42    pub bond: Box<Account<'info, Bond>>,
43
44    /// CHECK: holder record of the source owner. Read and written by hand because source and destination
45    /// may belong to the same owner.
46    #[account(mut, seeds = [HOLDER_SEED, mint.key().as_ref(), source_token.owner.as_ref()], bump)]
47    pub source_holder: UncheckedAccount<'info>,
48
49    /// CHECK: holder record of the destination owner (see above).
50    #[account(mut, seeds = [HOLDER_SEED, mint.key().as_ref(), destination_token.owner.as_ref()], bump)]
51    pub destination_holder: UncheckedAccount<'info>,
52}
53
54pub fn handle_transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {
55    let a = &ctx.accounts;
56    check_is_transferring(&a.source_token)?;
57    check_associated(&a.source_token, &a.mint)?;
58    check_associated(&a.destination_token, &a.mint)?;
59    let mut source = read_record(&a.source_holder.to_account_info())?;
60    let same = a.source_token.key() == a.destination_token.key();
61    let mut destination = if same { None } else { Some(read_record(&a.destination_holder.to_account_info())?) };
62    let bond = &a.bond;
63    let now = bond.now()?;
64    require!(now < bond.final_record_time()?, BondError::TransfersClosedAtMaturity);
65    if same { return Ok(()); }
66    let destination = destination.as_mut().unwrap();
67    let passed = bond.passed_record_moments_from(now, source.position.live_from.max(destination.position.live_from))?;
68    let source_before = a.source_token.amount.checked_add(amount).ok_or(BondError::Overflow)?;
69    let destination_before = a.destination_token.amount.checked_sub(amount).ok_or(BondError::Overflow)?;
70    update_record(&mut source, bond.key(), passed, source_before, a.source_token.amount)?;
71    require!(source.locked <= a.source_token.amount, BondError::BondsLocked);
72    update_record(destination, bond.key(), passed, destination_before, a.destination_token.amount)?;
73    source.try_serialize(&mut &mut a.source_holder.try_borrow_mut_data()?[..])?;
74    destination.try_serialize(&mut &mut a.destination_holder.try_borrow_mut_data()?[..])?;
75    Ok(())
76}
77
78fn read_record(info: &AccountInfo) -> Result<HolderRecord> {
79    require_keys_eq!(*info.owner, crate::ID, ErrorCode::AccountOwnedByWrongProgram);
80    HolderRecord::try_deserialize(&mut &info.try_borrow_data()?[..])
81}
82
83fn update_record(record: &mut HolderRecord, bond: Pubkey, passed: u16, before: u64, after: u64) -> Result<()> {
84    require!(record.position.quantity == before, BondError::PositionMismatch);
85    if let Some((from_event, to_event)) = record.position.save(passed) {
86        emit!(PositionsSaved { bond, owner: record.owner, from_event, to_event, quantity: before });
87    }
88    record.position.quantity = after;
89    Ok(())
90}
91
92fn check_associated(token: &InterfaceAccount<TokenAccount>, mint: &InterfaceAccount<Mint>) -> Result<()> {
93    let expected =
94        get_associated_token_address_with_program_id(&token.owner, &mint.key(), &anchor_spl::token_2022::ID);
95    require_keys_eq!(token.key(), expected, BondError::NotAssociatedTokenAccount);
96    Ok(())
97}
98
99/// From the official example: fail unless Token-2022 marked the account as being transferred.
100fn check_is_transferring(source: &InterfaceAccount<TokenAccount>) -> Result<()> {
101    let info = source.to_account_info();
102    let mut data: RefMut<&mut [u8]> = info.try_borrow_mut_data()?;
103    let mut account =
104        PodStateWithExtensionsMut::<PodAccount>::unpack(*data).map_err(|_| ProgramError::InvalidAccountData)?;
105    let extension =
106        account.get_extension_mut::<TransferHookAccount>().map_err(|_| ProgramError::InvalidAccountData)?;
107    require!(bool::from(extension.transferring), BondError::NotTransferring);
108    Ok(())
109}