diff --git a/programs/token-2022/src/instructions/extensions/memo_transfer/instructions/disable.rs b/programs/token-2022/src/instructions/extensions/memo_transfer/instructions/disable.rs new file mode 100644 index 00000000..2cf9e8a8 --- /dev/null +++ b/programs/token-2022/src/instructions/extensions/memo_transfer/instructions/disable.rs @@ -0,0 +1,144 @@ +use { + crate::{ + instructions::extensions::{ + memo_transfer::state::{ + offset_memo_transfer as OFFSET, InstructionDiscriminatorMemoTransfer, + }, + ExtensionDiscriminator, + }, + instructions::MAX_MULTISIG_SIGNERS, + }, + core::{mem::MaybeUninit, slice}, + pinocchio::{ + account_info::AccountInfo, + cpi::invoke_signed_with_bounds, + instruction::{AccountMeta, Instruction, Signer}, + program_error::ProgramError, + pubkey::Pubkey, + ProgramResult, + }, +}; + +/// Disable the MemoTransfer extension on a token account. +/// +/// Expected accounts: +/// +/// **Single authority** +/// 0. `[writable]` The token account to disable memo transfer. +/// 1. `[signer]` The owner of the token account. +/// +/// **Multisignature authority** +/// 0. `[writable]` The token account to disable memo transfer. +/// 1. `[readonly]` The multisig account that owns the token account. +/// 2. `[signer]` M signer accounts (as required by the multisig). +pub struct Disable<'a, 'b> { + /// The token account to disable with the MemoTransfer extension. + pub token_account: &'a AccountInfo, + /// The owner of the token account (single or multisig). + pub authority: &'a AccountInfo, + /// Signer accounts if the authority is a multisig. + pub signers: &'a [AccountInfo], + /// Token program (Token-2022). + pub token_program: &'b Pubkey, +} + +impl Disable<'_, '_> { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + #[inline(always)] + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + let &Self { + token_account, + authority, + signers: multisig_accounts, + token_program, + .. + } = self; + + if multisig_accounts.len() > MAX_MULTISIG_SIGNERS { + Err(ProgramError::InvalidArgument)?; + } + + const UNINIT_ACCOUNT_METAS: MaybeUninit = MaybeUninit::::uninit(); + let mut account_metas = [UNINIT_ACCOUNT_METAS; 2 + MAX_MULTISIG_SIGNERS]; + + unsafe { + // SAFETY: + // - `account_metas` is sized to 2 + MAX_MULTISIG_SIGNERS + + // - Index 0 is always present (TokenAccount) + account_metas + .get_unchecked_mut(0) + .write(AccountMeta::writable(token_account.key())); + + // - Index 1 is always present (Authority) + if multisig_accounts.is_empty() { + account_metas + .get_unchecked_mut(1) + .write(AccountMeta::readonly_signer(authority.key())); + } else { + account_metas + .get_unchecked_mut(1) + .write(AccountMeta::readonly(authority.key())); + } + } + + for (account_meta, signer) in account_metas[2..].iter_mut().zip(multisig_accounts.iter()) { + account_meta.write(AccountMeta::readonly_signer(signer.key())); + } + + // build instruction + let mut buffer = [0u8; OFFSET::END as usize]; + let data = disable_instruction_data(&mut buffer); + + let num_accounts = 2 + multisig_accounts.len(); + + let instruction = Instruction { + program_id: token_program, + data: data, + accounts: unsafe { slice::from_raw_parts(account_metas.as_ptr() as _, num_accounts) }, + }; + + // Account info array + const UNINIT_ACCOUNT_INFOS: MaybeUninit<&AccountInfo> = MaybeUninit::uninit(); + let mut account_infos = [UNINIT_ACCOUNT_INFOS; 2 + MAX_MULTISIG_SIGNERS]; + + unsafe { + // SAFETY: + // - `account_infos` is sized to 2 + MAX_MULTISIG_SIGNERS + // - Index 0 is always present + account_infos.get_unchecked_mut(0).write(token_account); + // - Index 1 is always present + account_infos.get_unchecked_mut(1).write(authority); + } + + // Fill signer accounts + for (account_info, signer) in account_infos[2..].iter_mut().zip(multisig_accounts.iter()) { + account_info.write(signer); + } + + invoke_signed_with_bounds::<{ 2 + MAX_MULTISIG_SIGNERS }>( + &instruction, + unsafe { + slice::from_raw_parts(account_infos.as_ptr() as *const &AccountInfo, num_accounts) + }, + signers, + ) + } +} + +#[inline(always)] +fn disable_instruction_data<'a>(buffer: &'a mut [u8]) -> &'a [u8] { + let offset = OFFSET::START as usize; + + // Encode discriminators (MemoTransfer + Disable) + buffer[..offset].copy_from_slice(&[ + ExtensionDiscriminator::MemoTransfer as u8, + InstructionDiscriminatorMemoTransfer::Disable as u8, + ]); + + buffer +} diff --git a/programs/token-2022/src/instructions/extensions/memo_transfer/instructions/enable.rs b/programs/token-2022/src/instructions/extensions/memo_transfer/instructions/enable.rs new file mode 100644 index 00000000..1e677a17 --- /dev/null +++ b/programs/token-2022/src/instructions/extensions/memo_transfer/instructions/enable.rs @@ -0,0 +1,144 @@ +use { + crate::{ + instructions::extensions::{ + memo_transfer::state::{ + offset_memo_transfer as OFFSET, InstructionDiscriminatorMemoTransfer, + }, + ExtensionDiscriminator, + }, + instructions::MAX_MULTISIG_SIGNERS, + }, + core::{mem::MaybeUninit, slice}, + pinocchio::{ + account_info::AccountInfo, + cpi::invoke_signed_with_bounds, + instruction::{AccountMeta, Instruction, Signer}, + program_error::ProgramError, + pubkey::Pubkey, + ProgramResult, + }, +}; + +/// Enable the MemoTransfer extension on a token account. +/// +/// Expected accounts: +/// +/// **Single authority** +/// 0. `[writable]` The token account to enable memo transfer. +/// 1. `[signer]` The owner of the token account. +/// +/// **Multisignature authority** +/// 0. `[writable]` The token account to enable memo transfer. +/// 1. `[readonly]` The multisig account that owns the token account. +/// 2. `[signer]` M signer accounts (as required by the multisig). +pub struct Enable<'a, 'b> { + /// The token account to enable with the MemoTransfer extension. + pub token_account: &'a AccountInfo, + /// The owner of the token account (single or multisig). + pub authority: &'a AccountInfo, + /// Signer accounts if the authority is a multisig. + pub signers: &'a [AccountInfo], + /// Token program (Token-2022). + pub token_program: &'b Pubkey, +} + +impl Enable<'_, '_> { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + #[inline(always)] + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + let &Self { + token_account, + authority, + signers: multisig_accounts, + token_program, + .. + } = self; + + if multisig_accounts.len() > MAX_MULTISIG_SIGNERS { + Err(ProgramError::InvalidArgument)?; + } + + const UNINIT_ACCOUNT_METAS: MaybeUninit = MaybeUninit::::uninit(); + let mut account_metas = [UNINIT_ACCOUNT_METAS; 2 + MAX_MULTISIG_SIGNERS]; + + unsafe { + // SAFETY: + // - `account_metas` is sized to 2 + MAX_MULTISIG_SIGNERS + + // - Index 0 is always present (TokenAccount) + account_metas + .get_unchecked_mut(0) + .write(AccountMeta::writable(token_account.key())); + + // - Index 1 is always present (Authority) + if multisig_accounts.is_empty() { + account_metas + .get_unchecked_mut(1) + .write(AccountMeta::readonly_signer(authority.key())); + } else { + account_metas + .get_unchecked_mut(1) + .write(AccountMeta::readonly(authority.key())); + } + } + + for (account_meta, signer) in account_metas[2..].iter_mut().zip(multisig_accounts.iter()) { + account_meta.write(AccountMeta::readonly_signer(signer.key())); + } + + // build instruction + let mut buffer = [0u8; OFFSET::END as usize]; + let data = enable_instruction_data(&mut buffer); + + let num_accounts = 2 + multisig_accounts.len(); + + let instruction = Instruction { + program_id: token_program, + data: data, + accounts: unsafe { slice::from_raw_parts(account_metas.as_ptr() as _, num_accounts) }, + }; + + // Account info array + const UNINIT_ACCOUNT_INFOS: MaybeUninit<&AccountInfo> = MaybeUninit::uninit(); + let mut account_infos = [UNINIT_ACCOUNT_INFOS; 2 + MAX_MULTISIG_SIGNERS]; + + unsafe { + // SAFETY: + // - `account_infos` is sized to 2 + MAX_MULTISIG_SIGNERS + // - Index 0 is always present + account_infos.get_unchecked_mut(0).write(token_account); + // - Index 1 is always present + account_infos.get_unchecked_mut(1).write(authority); + } + + // Fill signer accounts + for (account_info, signer) in account_infos[2..].iter_mut().zip(multisig_accounts.iter()) { + account_info.write(signer); + } + + invoke_signed_with_bounds::<{ 2 + MAX_MULTISIG_SIGNERS }>( + &instruction, + unsafe { + slice::from_raw_parts(account_infos.as_ptr() as *const &AccountInfo, num_accounts) + }, + signers, + ) + } +} + +#[inline(always)] +fn enable_instruction_data<'a>(buffer: &'a mut [u8]) -> &'a [u8] { + let offset = OFFSET::START as usize; + + // Encode discriminators (MemoTransfer + Enable) + buffer[..offset].copy_from_slice(&[ + ExtensionDiscriminator::MemoTransfer as u8, + InstructionDiscriminatorMemoTransfer::Enable as u8, + ]); + + buffer +} diff --git a/programs/token-2022/src/instructions/extensions/memo_transfer/instructions/mod.rs b/programs/token-2022/src/instructions/extensions/memo_transfer/instructions/mod.rs new file mode 100644 index 00000000..5835cb6f --- /dev/null +++ b/programs/token-2022/src/instructions/extensions/memo_transfer/instructions/mod.rs @@ -0,0 +1,4 @@ +pub mod disable; +pub mod enable; + +pub use {disable::*, enable::*}; diff --git a/programs/token-2022/src/instructions/extensions/memo_transfer/mod.rs b/programs/token-2022/src/instructions/extensions/memo_transfer/mod.rs new file mode 100644 index 00000000..87a1fb46 --- /dev/null +++ b/programs/token-2022/src/instructions/extensions/memo_transfer/mod.rs @@ -0,0 +1,4 @@ +pub mod instructions; +pub mod state; + +pub use {instructions::*, state::*}; diff --git a/programs/token-2022/src/instructions/extensions/memo_transfer/state.rs b/programs/token-2022/src/instructions/extensions/memo_transfer/state.rs new file mode 100644 index 00000000..4ced148a --- /dev/null +++ b/programs/token-2022/src/instructions/extensions/memo_transfer/state.rs @@ -0,0 +1,24 @@ +#[repr(u8)] +pub enum InstructionDiscriminatorMemoTransfer { + Enable = 0, + Disable = 1, +} + +/// Instruction data layout: +/// - [0] : Extension discriminator (1 byte) +/// - [1] : Instruction discriminator (1 byte) +/// dev: Since only the instruction discriminator is used to toggle memo transfer states +/// and no additional parameters are required, `START == END`. +pub mod offset_memo_transfer { + pub const START: u8 = 2; + pub const END: u8 = START; +} + +/// Models onchain `MemoTransfer` state. +/// Mirrors SPL Token-2022: +/// `pub struct MemoTransfer { pub require_incoming_transfer_memos: PodBool }` +#[repr(C)] +pub struct MemoTransfer { + /// Indicates whether incoming transfers must include a memo. + pub require_incoming_transfer_memos: bool, +}