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}