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

Commit 0268d76

Browse files
author
Tyera Eulberg
authored
token-2022: complete memo extension functionality (#2876)
* Add NoMemo error and use syscall to require memos * Flesh out + dedupe memo-transfer tests * Add memo v1 test cases
1 parent d505f2a commit 0268d76

File tree

6 files changed

+133
-9
lines changed

6 files changed

+133
-9
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

token/program-2022-test/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ async-trait = "0.1"
1818
solana-program-test = "=1.9.9"
1919
solana-sdk = "=1.9.9"
2020
spl-associated-token-account = { version = "1.0.5", path = "../../associated-token-account/program" }
21+
spl-memo = { version = "3.0.1", path = "../../memo/program", features = ["no-entrypoint"] }
2122
spl-token-2022 = { version = "0.2", path="../program-2022", features = ["no-entrypoint"] }
2223
spl-token-client = { version = "0.0.1", path = "../client" }

token/program-2022-test/tests/memo_transfer.rs

Lines changed: 112 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,28 @@
33
mod program_test;
44
use {
55
program_test::{TestContext, TokenContext},
6-
solana_program_test::tokio,
7-
solana_sdk::{pubkey::Pubkey, signature::Signer},
8-
spl_token_2022::extension::{memo_transfer::MemoTransfer, ExtensionType},
6+
solana_program_test::{
7+
tokio::{self, sync::Mutex},
8+
ProgramTestContext,
9+
},
10+
solana_sdk::{
11+
instruction::InstructionError,
12+
pubkey::Pubkey,
13+
signature::Signer,
14+
system_instruction,
15+
transaction::{Transaction, TransactionError},
16+
transport::TransportError,
17+
},
18+
spl_token_2022::{
19+
error::TokenError,
20+
extension::{memo_transfer::MemoTransfer, ExtensionType},
21+
},
22+
spl_token_client::token::TokenError as TokenClientError,
23+
std::sync::Arc,
924
};
1025

1126
async fn test_memo_transfers(
27+
context: Arc<Mutex<ProgramTestContext>>,
1228
token_context: TokenContext,
1329
alice_account: Pubkey,
1430
bob_account: Pubkey,
@@ -38,14 +54,102 @@ async fn test_memo_transfers(
3854
assert!(bool::from(extension.require_incoming_transfer_memos));
3955

4056
// attempt to transfer from alice to bob without memo
41-
// TODO: should fail when token/program-2022/src/processor.rs#L376 is completed
57+
let err = token
58+
.transfer_unchecked(&alice_account, &bob_account, &alice, 10)
59+
.await
60+
.unwrap_err();
61+
assert_eq!(
62+
err,
63+
TokenClientError::Client(Box::new(TransportError::TransactionError(
64+
TransactionError::InstructionError(
65+
0,
66+
InstructionError::Custom(TokenError::NoMemo as u32)
67+
)
68+
)))
69+
);
70+
let bob_state = token.get_account_info(&bob_account).await.unwrap();
71+
assert_eq!(bob_state.base.amount, 0);
72+
73+
// attempt to transfer from alice to bob with misplaced memo, v1 and current
74+
let mut memo_ix = spl_memo::build_memo(&[240, 159, 166, 150], &[]);
75+
for program_id in [spl_memo::id(), spl_memo::v1::id()] {
76+
let mut ctx = context.lock().await;
77+
memo_ix.program_id = program_id;
78+
#[allow(deprecated)]
79+
let instructions = vec![
80+
memo_ix.clone(),
81+
system_instruction::transfer(&ctx.payer.pubkey(), &alice.pubkey(), 42),
82+
spl_token_2022::instruction::transfer(
83+
&spl_token_2022::id(),
84+
&alice_account,
85+
&bob_account,
86+
&alice.pubkey(),
87+
&[],
88+
10,
89+
)
90+
.unwrap(),
91+
];
92+
let tx = Transaction::new_signed_with_payer(
93+
&instructions,
94+
Some(&ctx.payer.pubkey()),
95+
&[&ctx.payer, &alice],
96+
ctx.last_blockhash,
97+
);
98+
let err: TransactionError = ctx
99+
.banks_client
100+
.process_transaction(tx)
101+
.await
102+
.unwrap_err()
103+
.unwrap()
104+
.into();
105+
drop(ctx);
106+
assert_eq!(
107+
err,
108+
TransactionError::InstructionError(
109+
2,
110+
InstructionError::Custom(TokenError::NoMemo as u32)
111+
)
112+
);
113+
let bob_state = token.get_account_info(&bob_account).await.unwrap();
114+
assert_eq!(bob_state.base.amount, 0);
115+
}
116+
117+
// transfer with memo
42118
token
119+
.with_memo("🦖")
43120
.transfer_unchecked(&alice_account, &bob_account, &alice, 10)
44121
.await
45122
.unwrap();
46123
let bob_state = token.get_account_info(&bob_account).await.unwrap();
47124
assert_eq!(bob_state.base.amount, 10);
48125

126+
// transfer with memo v1
127+
let mut ctx = context.lock().await;
128+
memo_ix.program_id = spl_memo::v1::id();
129+
#[allow(deprecated)]
130+
let instructions = vec![
131+
memo_ix,
132+
spl_token_2022::instruction::transfer(
133+
&spl_token_2022::id(),
134+
&alice_account,
135+
&bob_account,
136+
&alice.pubkey(),
137+
&[],
138+
11,
139+
)
140+
.unwrap(),
141+
];
142+
let tx = Transaction::new_signed_with_payer(
143+
&instructions,
144+
Some(&ctx.payer.pubkey()),
145+
&[&ctx.payer, &alice],
146+
ctx.last_blockhash,
147+
);
148+
ctx.banks_client.process_transaction(tx).await.unwrap();
149+
drop(ctx);
150+
let bob_state = token.get_account_info(&bob_account).await.unwrap();
151+
assert_eq!(bob_state.base.amount, 21);
152+
49153
// stop requiring memo transfers into bob_account
50154
token
51155
.disable_required_transfer_memos(&bob_account, &bob)
@@ -54,11 +158,11 @@ async fn test_memo_transfers(
54158

55159
// transfer from alice to bob without memo
56160
token
57-
.transfer_unchecked(&alice_account, &bob_account, &alice, 11)
161+
.transfer_unchecked(&alice_account, &bob_account, &alice, 12)
58162
.await
59163
.unwrap();
60164
let bob_state = token.get_account_info(&bob_account).await.unwrap();
61-
assert_eq!(bob_state.base.amount, 21);
165+
assert_eq!(bob_state.base.amount, 33);
62166
}
63167

64168
#[tokio::test]
@@ -83,7 +187,7 @@ async fn require_memo_transfers_without_realloc() {
83187
.await
84188
.unwrap();
85189

86-
test_memo_transfers(token_context, alice_account, bob_account).await;
190+
test_memo_transfers(context.context, token_context, alice_account, bob_account).await;
87191
}
88192

89193
#[tokio::test]
@@ -113,5 +217,5 @@ async fn require_memo_transfers_with_realloc() {
113217
.await
114218
.unwrap();
115219

116-
test_memo_transfers(token_context, alice_account, bob_account).await;
220+
test_memo_transfers(context.context, token_context, alice_account, bob_account).await;
117221
}

token/program-2022/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ num-traits = "0.2"
2020
num_enum = "0.5.4"
2121
solana-program = "1.9.9"
2222
solana-zk-token-sdk = "0.6.0"
23+
spl-memo = { version = "3.0.1", path = "../../memo/program", features = [ "no-entrypoint" ] }
2324
spl-token = { version = "3.3", path = "../program", features = ["no-entrypoint"] }
2425
thiserror = "1.0"
2526

token/program-2022/src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ pub enum TokenError {
135135
/// mint and try again
136136
#[error("An account can only be closed if its withheld fee balance is zero, harvest fees to the mint and try again")]
137137
AccountHasWithheldTransferFees,
138+
/// No memo in previous instruction; required for recipient to receive a transfer
139+
#[error("No memo in previous instruction; required for recipient to receive a transfer")]
140+
NoMemo,
138141
}
139142
impl From<TokenError> for ProgramError {
140143
fn from(e: TokenError) -> Self {

token/program-2022/src/processor.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use {
2424
clock::Clock,
2525
decode_error::DecodeError,
2626
entrypoint::ProgramResult,
27+
instruction::get_processed_sibling_instruction,
2728
msg,
2829
program::{invoke, invoke_signed, set_return_data},
2930
program_error::{PrintProgramError, ProgramError},
@@ -377,7 +378,16 @@ impl Processor {
377378
}
378379

379380
if memo_required(&dest_account) {
380-
// TODO: use get_processed_instructions syscall to check for memo
381+
let is_memo_program = |program_id: &Pubkey| -> bool {
382+
program_id == &spl_memo::id() || program_id == &spl_memo::v1::id()
383+
};
384+
let previous_instruction = get_processed_sibling_instruction(0);
385+
match previous_instruction {
386+
Some(instruction) if is_memo_program(&instruction.program_id) => {}
387+
_ => {
388+
return Err(TokenError::NoMemo.into());
389+
}
390+
}
381391
}
382392

383393
source_account.base.amount = source_account
@@ -1398,6 +1408,9 @@ impl PrintProgramError for TokenError {
13981408
TokenError::AccountHasWithheldTransferFees => {
13991409
msg!("Error: An account can only be closed if its withheld fee balance is zero, harvest fees to the mint and try again");
14001410
}
1411+
TokenError::NoMemo => {
1412+
msg!("Error: No memo in previous instruction; required for recipient to receive a transfer");
1413+
}
14011414
}
14021415
}
14031416
}

0 commit comments

Comments
 (0)