Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 482a928

Browse files
token-2022: Add MemoTransfer extension (#2822)
* Style nits * Initial MemoTransfer extension * Stub in check for memo * Add memo-transfer token-client method * Add MemoTransfer tests * Add immutable get_extension, and clean up mod * Update token/program-2022/src/extension/memo_transfer/instruction.rs Co-authored-by: Jon Cinque <[email protected]> * Update token/rust/src/token.rs Co-authored-by: Jon Cinque <[email protected]> Co-authored-by: Jon Cinque <[email protected]>
1 parent c7ec442 commit 482a928

File tree

11 files changed

+473
-8
lines changed

11 files changed

+473
-8
lines changed

token/program-2022/src/extension/confidential_transfer/instruction.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ pub enum ConfidentialTransferInstruction {
3434
/// Accounts expected by this instruction:
3535
///
3636
/// 0. `[writable]` The SPL Token mint
37-
//
37+
///
3838
/// Data expected by this instruction:
3939
/// `ConfidentialTransferMint`
4040
///

token/program-2022/src/extension/default_account_state/instruction.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ pub enum DefaultAccountStateInstruction {
2828
/// Accounts expected by this instruction:
2929
///
3030
/// 0. `[writable]` The mint to initialize.
31-
//
31+
///
3232
/// Data expected by this instruction:
3333
/// `crate::state::AccountState`
3434
///
@@ -46,7 +46,7 @@ pub enum DefaultAccountStateInstruction {
4646
/// 0. `[writable]` The mint.
4747
/// 1. `[]` The mint's multisignature freeze authority.
4848
/// 2. ..2+M `[signer]` M signer accounts.
49-
//
49+
///
5050
/// Data expected by this instruction:
5151
/// `crate::state::AccountState`
5252
///
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
use {
2+
crate::{check_program_account, error::TokenError, instruction::TokenInstruction},
3+
num_enum::{IntoPrimitive, TryFromPrimitive},
4+
solana_program::{
5+
instruction::{AccountMeta, Instruction},
6+
program_error::ProgramError,
7+
pubkey::Pubkey,
8+
},
9+
std::convert::TryFrom,
10+
};
11+
12+
/// Default Account State extension instructions
13+
#[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)]
14+
#[repr(u8)]
15+
pub enum RequiredMemoTransfersInstruction {
16+
/// Require memos for transfers into this Account. Adds the MemoTransfer extension to the
17+
/// Account, if it doesn't already exist.
18+
///
19+
/// Accounts expected by this instruction:
20+
///
21+
/// 0. `[writable]` The account to update.
22+
/// 1. `[signer]` The account's owner.
23+
///
24+
/// * Multisignature authority
25+
/// 0. `[writable]` The account to update.
26+
/// 1. `[]` The account's multisignature owner.
27+
/// 2. ..2+M `[signer]` M signer accounts.
28+
///
29+
Enable,
30+
/// Stop requiring memos for transfers into this Account.
31+
///
32+
/// Fails if the account does not have the extension present.
33+
///
34+
/// Accounts expected by this instruction:
35+
///
36+
/// 0. `[writable]` The account to update.
37+
/// 1. `[signer]` The account's owner.
38+
///
39+
/// * Multisignature authority
40+
/// 0. `[writable]` The account to update.
41+
/// 1. `[]` The account's multisignature owner.
42+
/// 2. ..2+M `[signer]` M signer accounts.
43+
///
44+
Disable,
45+
}
46+
47+
pub(crate) fn decode_instruction(
48+
input: &[u8],
49+
) -> Result<RequiredMemoTransfersInstruction, ProgramError> {
50+
if input.len() != 1 {
51+
return Err(TokenError::InvalidInstruction.into());
52+
}
53+
RequiredMemoTransfersInstruction::try_from(input[0])
54+
.map_err(|_| TokenError::InvalidInstruction.into())
55+
}
56+
57+
fn encode_instruction(
58+
token_program_id: &Pubkey,
59+
accounts: Vec<AccountMeta>,
60+
instruction_type: RequiredMemoTransfersInstruction,
61+
) -> Instruction {
62+
let mut data = TokenInstruction::MemoTransferExtension.pack();
63+
data.push(instruction_type.into());
64+
Instruction {
65+
program_id: *token_program_id,
66+
accounts,
67+
data,
68+
}
69+
}
70+
71+
/// Create an `Enable` instruction
72+
pub fn enable_required_transfer_memos(
73+
token_program_id: &Pubkey,
74+
account: &Pubkey,
75+
owner: &Pubkey,
76+
signers: &[&Pubkey],
77+
) -> Result<Instruction, ProgramError> {
78+
check_program_account(token_program_id)?;
79+
let mut accounts = vec![
80+
AccountMeta::new(*account, false),
81+
AccountMeta::new_readonly(*owner, signers.is_empty()),
82+
];
83+
for signer_pubkey in signers.iter() {
84+
accounts.push(AccountMeta::new_readonly(**signer_pubkey, true));
85+
}
86+
Ok(encode_instruction(
87+
token_program_id,
88+
accounts,
89+
RequiredMemoTransfersInstruction::Enable,
90+
))
91+
}
92+
93+
/// Create a `Disable` instruction
94+
pub fn disable_required_transfer_memos(
95+
token_program_id: &Pubkey,
96+
account: &Pubkey,
97+
owner: &Pubkey,
98+
signers: &[&Pubkey],
99+
) -> Result<Instruction, ProgramError> {
100+
check_program_account(token_program_id)?;
101+
let mut accounts = vec![
102+
AccountMeta::new(*account, false),
103+
AccountMeta::new_readonly(*owner, signers.is_empty()),
104+
];
105+
for signer_pubkey in signers.iter() {
106+
accounts.push(AccountMeta::new_readonly(**signer_pubkey, true));
107+
}
108+
Ok(encode_instruction(
109+
token_program_id,
110+
accounts,
111+
RequiredMemoTransfersInstruction::Disable,
112+
))
113+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use {
2+
crate::{
3+
extension::{Extension, ExtensionType, StateWithExtensionsMut},
4+
pod::PodBool,
5+
state::Account,
6+
},
7+
bytemuck::{Pod, Zeroable},
8+
};
9+
10+
/// Memo Transfer extension instructions
11+
pub mod instruction;
12+
13+
/// Memo Transfer extension processor
14+
pub mod processor;
15+
16+
/// Memo Transfer extension for Accounts
17+
#[repr(C)]
18+
#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
19+
pub struct MemoTransfer {
20+
/// Require transfers into this account to be accompanied by a memo
21+
pub require_incoming_transfer_memos: PodBool,
22+
}
23+
impl Extension for MemoTransfer {
24+
const TYPE: ExtensionType = ExtensionType::MemoTransfer;
25+
}
26+
27+
/// Determine if a memo is required for transfers into this account
28+
pub fn memo_required(account_state: &StateWithExtensionsMut<Account>) -> bool {
29+
if let Ok(extension) = account_state.get_extension::<MemoTransfer>() {
30+
return extension.require_incoming_transfer_memos.into();
31+
}
32+
false
33+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
use {
2+
crate::{
3+
check_program_account,
4+
extension::{
5+
memo_transfer::{
6+
instruction::{decode_instruction, RequiredMemoTransfersInstruction},
7+
MemoTransfer,
8+
},
9+
StateWithExtensionsMut,
10+
},
11+
processor::Processor,
12+
state::Account,
13+
},
14+
solana_program::{
15+
account_info::{next_account_info, AccountInfo},
16+
entrypoint::ProgramResult,
17+
msg,
18+
pubkey::Pubkey,
19+
},
20+
};
21+
22+
fn process_enable_required_memo_transfers(
23+
program_id: &Pubkey,
24+
accounts: &[AccountInfo],
25+
) -> ProgramResult {
26+
let account_info_iter = &mut accounts.iter();
27+
let token_account_info = next_account_info(account_info_iter)?;
28+
let owner_info = next_account_info(account_info_iter)?;
29+
let owner_info_data_len = owner_info.data_len();
30+
31+
let mut account_data = token_account_info.data.borrow_mut();
32+
let mut account = StateWithExtensionsMut::<Account>::unpack(&mut account_data)?;
33+
34+
Processor::validate_owner(
35+
program_id,
36+
&account.base.owner,
37+
owner_info,
38+
owner_info_data_len,
39+
account_info_iter.as_slice(),
40+
)?;
41+
42+
let extension = if let Ok(extension) = account.get_extension_mut::<MemoTransfer>() {
43+
extension
44+
} else {
45+
account.init_extension::<MemoTransfer>()?
46+
};
47+
extension.require_incoming_transfer_memos = true.into();
48+
Ok(())
49+
}
50+
51+
fn process_diasble_required_memo_transfers(
52+
program_id: &Pubkey,
53+
accounts: &[AccountInfo],
54+
) -> ProgramResult {
55+
let account_info_iter = &mut accounts.iter();
56+
let token_account_info = next_account_info(account_info_iter)?;
57+
let owner_info = next_account_info(account_info_iter)?;
58+
let owner_info_data_len = owner_info.data_len();
59+
60+
let mut account_data = token_account_info.data.borrow_mut();
61+
let mut account = StateWithExtensionsMut::<Account>::unpack(&mut account_data)?;
62+
63+
Processor::validate_owner(
64+
program_id,
65+
&account.base.owner,
66+
owner_info,
67+
owner_info_data_len,
68+
account_info_iter.as_slice(),
69+
)?;
70+
71+
let extension = if let Ok(extension) = account.get_extension_mut::<MemoTransfer>() {
72+
extension
73+
} else {
74+
account.init_extension::<MemoTransfer>()?
75+
};
76+
extension.require_incoming_transfer_memos = false.into();
77+
Ok(())
78+
}
79+
80+
pub(crate) fn process_instruction(
81+
program_id: &Pubkey,
82+
accounts: &[AccountInfo],
83+
input: &[u8],
84+
) -> ProgramResult {
85+
check_program_account(program_id)?;
86+
87+
let instruction = decode_instruction(input)?;
88+
match instruction {
89+
RequiredMemoTransfersInstruction::Enable => {
90+
msg!("RequiredMemoTransfersInstruction::Enable");
91+
process_enable_required_memo_transfers(program_id, accounts)
92+
}
93+
RequiredMemoTransfersInstruction::Disable => {
94+
msg!("RequiredMemoTransfersInstruction::Disable");
95+
process_diasble_required_memo_transfers(program_id, accounts)
96+
}
97+
}
98+
}

token/program-2022/src/extension/mod.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use {
77
confidential_transfer::{ConfidentialTransferAccount, ConfidentialTransferMint},
88
default_account_state::DefaultAccountState,
99
immutable_owner::ImmutableOwner,
10+
memo_transfer::MemoTransfer,
1011
mint_close_authority::MintCloseAuthority,
1112
transfer_fee::{TransferFeeAmount, TransferFeeConfig},
1213
},
@@ -31,6 +32,8 @@ pub mod confidential_transfer;
3132
pub mod default_account_state;
3233
/// Immutable Owner extension
3334
pub mod immutable_owner;
35+
/// Memo Transfer extension
36+
pub mod memo_transfer;
3437
/// Mint Close Authority extension
3538
pub mod mint_close_authority;
3639
/// Utility to reallocate token accounts
@@ -431,11 +434,30 @@ impl<'data, S: BaseState> StateWithExtensionsMut<'data, S> {
431434
}
432435
}
433436

434-
/// Unpack a portion of the TLV data as the desired type
437+
/// Unpack a portion of the TLV data as the desired type that allows modifying the type
435438
pub fn get_extension_mut<V: Extension>(&mut self) -> Result<&mut V, ProgramError> {
436439
self.init_or_get_extension(false)
437440
}
438441

442+
/// Unpack a portion of the TLV data as the desired type
443+
pub fn get_extension<V: Extension>(&self) -> Result<&V, ProgramError> {
444+
if V::TYPE.get_account_type() != S::ACCOUNT_TYPE {
445+
return Err(ProgramError::InvalidAccountData);
446+
}
447+
let TlvIndices {
448+
type_start,
449+
length_start,
450+
value_start,
451+
} = get_extension_indices::<V>(self.tlv_data, false)?;
452+
453+
if self.tlv_data[type_start..].len() < V::TYPE.get_tlv_len() {
454+
return Err(ProgramError::InvalidAccountData);
455+
}
456+
let length = pod_from_bytes::<Length>(&self.tlv_data[length_start..value_start])?;
457+
let value_end = value_start.saturating_add(usize::from(*length));
458+
pod_from_bytes::<V>(&self.tlv_data[value_start..value_end])
459+
}
460+
439461
/// Packs base state data into the base data portion
440462
pub fn pack_base(&mut self) {
441463
S::pack_into_slice(&self.base, self.base_data);
@@ -561,6 +583,8 @@ pub enum ExtensionType {
561583
DefaultAccountState,
562584
/// Indicates that the Account owner authority cannot be changed
563585
ImmutableOwner,
586+
/// Require inbound transfers to have memo
587+
MemoTransfer,
564588
/// Padding extension used to make an account exactly Multisig::LEN, used for testing
565589
#[cfg(test)]
566590
AccountPaddingTest = u16::MAX - 1,
@@ -598,6 +622,7 @@ impl ExtensionType {
598622
pod_get_packed_len::<ConfidentialTransferAccount>()
599623
}
600624
ExtensionType::DefaultAccountState => pod_get_packed_len::<DefaultAccountState>(),
625+
ExtensionType::MemoTransfer => pod_get_packed_len::<MemoTransfer>(),
601626
#[cfg(test)]
602627
ExtensionType::AccountPaddingTest => pod_get_packed_len::<AccountPaddingTest>(),
603628
#[cfg(test)]
@@ -655,7 +680,8 @@ impl ExtensionType {
655680
| ExtensionType::DefaultAccountState => AccountType::Mint,
656681
ExtensionType::ImmutableOwner
657682
| ExtensionType::TransferFeeAmount
658-
| ExtensionType::ConfidentialTransferAccount => AccountType::Account,
683+
| ExtensionType::ConfidentialTransferAccount
684+
| ExtensionType::MemoTransfer => AccountType::Account,
659685
#[cfg(test)]
660686
ExtensionType::AccountPaddingTest => AccountType::Account,
661687
#[cfg(test)]

token/program-2022/src/instruction.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,11 @@ pub enum TokenInstruction {
522522
/// New extension types to include in the reallocated account
523523
extension_types: Vec<ExtensionType>,
524524
},
525+
/// The common instruction prefix for Memo Transfer account extension instructions.
526+
///
527+
/// See `extension::memo_transfer::instruction::RequiredMemoTransfersInstruction` for
528+
/// further details about the extended instructions that share this instruction prefix
529+
MemoTransferExtension,
525530
}
526531
impl TokenInstruction {
527532
/// Unpacks a byte buffer into a [TokenInstruction](enum.TokenInstruction.html).
@@ -640,6 +645,7 @@ impl TokenInstruction {
640645
}
641646
Self::Reallocate { extension_types }
642647
}
648+
28 => Self::MemoTransferExtension,
643649
_ => return Err(TokenError::InvalidInstruction.into()),
644650
})
645651
}
@@ -772,6 +778,9 @@ impl TokenInstruction {
772778
buf.extend_from_slice(&<[u8; 2]>::from(*extension_type));
773779
}
774780
}
781+
&Self::MemoTransferExtension => {
782+
buf.push(28);
783+
}
775784
};
776785
buf
777786
}

token/program-2022/src/pod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ impl From<&PodBool> for bool {
7474
}
7575
}
7676

77+
impl From<PodBool> for bool {
78+
fn from(b: PodBool) -> Self {
79+
b.0 != 0
80+
}
81+
}
82+
7783
/// The standard `u16` can cause alignment issues when placed in a `Pod`, define a replacement that
7884
/// is usable in all `Pod`s
7985
#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]

0 commit comments

Comments
 (0)