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

Commit 3e6a9b8

Browse files
author
Joe C
authored
token 2022: add new offchain helper
This PR adds a new offchain helper for creating a `TransferChecked` instruction with the necessary accounts metas for a transfer hook `ExecuteInstruction` to Token2022, deprecating the old one. As described in #6064, the original offchain helper in Token2022 was being used incorrectly to resolve extra account metas. Rather than providing the SPL Transfer Hook interface helper with a valid `ExecuteInstruction`, the original Token2022 helper was actually providing it with a transfer instruction, causing erroneous account resolution. Taking advantage of the more secure SPL Transfer Hook interface offchain helper provided in #6099, this new offchain helper creates a `TransferChecked` instruction and calls `add_extra_account_metas_for_execute(..)`, providing the keys used to build the transfer instruction. Note: unlike the deprecated helper in #6099, the deprecated offchain helper in Token2022 *is* in fact inaccurately resolving account metas for certain use cases, thus it should be vigilantly avoided.
1 parent 8076018 commit 3e6a9b8

File tree

5 files changed

+260
-0
lines changed

5 files changed

+260
-0
lines changed

Cargo.lock

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

token/client/src/token.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,6 +938,7 @@ where
938938
if let Some(transfer_hook_accounts) = &self.transfer_hook_accounts {
939939
instruction.accounts.extend(transfer_hook_accounts.clone());
940940
} else {
941+
#[allow(deprecated)]
941942
offchain::resolve_extra_transfer_account_metas(
942943
&mut instruction,
943944
|address| {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,7 @@ async fn success_downgrade_writable_and_signer_accounts() {
627627
.unwrap();
628628
}
629629

630+
#[allow(deprecated)]
630631
#[tokio::test]
631632
async fn success_transfers_using_onchain_helper() {
632633
let authority = Pubkey::new_unique();

token/program-2022/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ proptest = "1.4"
4545
serial_test = "3.0.0"
4646
solana-program-test = "1.17.6"
4747
solana-sdk = "1.17.6"
48+
spl-tlv-account-resolution = { version = "0.5.0", path = "../../libraries/tlv-account-resolution" }
4849
serde_json = "1.0.111"
4950

5051
[lib]

token/program-2022/src/offchain.rs

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use {
77
state::Mint,
88
},
99
solana_program::{instruction::Instruction, program_error::ProgramError, pubkey::Pubkey},
10+
spl_transfer_hook_interface::offchain::add_extra_account_metas_for_execute,
1011
std::future::Future,
1112
};
1213

@@ -32,6 +33,10 @@ use {
3233
/// &mint,
3334
/// ).await?;
3435
/// ```
36+
#[deprecated(
37+
since = "1.1.0",
38+
note = "Please use `create_transfer_checked_instruction_with_extra_metas` instead"
39+
)]
3540
pub async fn resolve_extra_transfer_account_metas<F, Fut>(
3641
instruction: &mut Instruction,
3742
fetch_account_data_fn: F,
@@ -57,3 +62,254 @@ where
5762
}
5863
Ok(())
5964
}
65+
66+
/// Offchain helper to create a `TransferChecked` instruction with all
67+
/// additional required account metas for a transfer, including the ones
68+
/// required by the transfer hook.
69+
///
70+
/// To be client-agnostic and to avoid pulling in the full solana-sdk, this
71+
/// simply takes a function that will return its data as `Future<Vec<u8>>` for
72+
/// the given address. Can be called in the following way:
73+
///
74+
/// ```rust,ignore
75+
/// let instruction = create_transfer_checked_instruction_with_extra_metas(
76+
/// &spl_token_2022::id(),
77+
/// &source,
78+
/// &mint,
79+
/// &destination,
80+
/// &authority,
81+
/// &[],
82+
/// amount,
83+
/// decimals,
84+
/// |address| self.client.get_account(&address).map_ok(|opt| opt.map(|acc| acc.data)),
85+
/// )
86+
/// .await?
87+
/// ```
88+
#[allow(clippy::too_many_arguments)]
89+
pub async fn create_transfer_checked_instruction_with_extra_metas<F, Fut>(
90+
token_program_id: &Pubkey,
91+
source_pubkey: &Pubkey,
92+
mint_pubkey: &Pubkey,
93+
destination_pubkey: &Pubkey,
94+
authority_pubkey: &Pubkey,
95+
signer_pubkeys: &[&Pubkey],
96+
amount: u64,
97+
decimals: u8,
98+
fetch_account_data_fn: F,
99+
) -> Result<Instruction, AccountFetchError>
100+
where
101+
F: Fn(Pubkey) -> Fut,
102+
Fut: Future<Output = AccountDataResult>,
103+
{
104+
let mut transfer_instruction = crate::instruction::transfer_checked(
105+
token_program_id,
106+
source_pubkey,
107+
mint_pubkey,
108+
destination_pubkey,
109+
authority_pubkey,
110+
signer_pubkeys,
111+
amount,
112+
decimals,
113+
)?;
114+
115+
let mint_data = fetch_account_data_fn(*mint_pubkey)
116+
.await?
117+
.ok_or(ProgramError::InvalidAccountData)?;
118+
let mint = StateWithExtensions::<Mint>::unpack(&mint_data)?;
119+
120+
if let Some(program_id) = transfer_hook::get_program_id(&mint) {
121+
add_extra_account_metas_for_execute(
122+
&mut transfer_instruction,
123+
&program_id,
124+
source_pubkey,
125+
mint_pubkey,
126+
destination_pubkey,
127+
authority_pubkey,
128+
amount,
129+
fetch_account_data_fn,
130+
)
131+
.await?;
132+
}
133+
134+
Ok(transfer_instruction)
135+
}
136+
137+
#[cfg(test)]
138+
mod tests {
139+
use {
140+
super::*,
141+
crate::extension::{transfer_hook::TransferHook, ExtensionType, StateWithExtensionsMut},
142+
solana_program::{instruction::AccountMeta, program_option::COption},
143+
solana_program_test::tokio,
144+
spl_pod::optional_keys::OptionalNonZeroPubkey,
145+
spl_tlv_account_resolution::{
146+
account::ExtraAccountMeta, seeds::Seed, state::ExtraAccountMetaList,
147+
},
148+
spl_transfer_hook_interface::{
149+
get_extra_account_metas_address, instruction::ExecuteInstruction,
150+
},
151+
};
152+
153+
const DECIMALS: u8 = 0;
154+
const MINT_PUBKEY: Pubkey = Pubkey::new_from_array([1u8; 32]);
155+
const TRANSFER_HOOK_PROGRAM_ID: Pubkey = Pubkey::new_from_array([2u8; 32]);
156+
const EXTRA_META_1: Pubkey = Pubkey::new_from_array([3u8; 32]);
157+
const EXTRA_META_2: Pubkey = Pubkey::new_from_array([4u8; 32]);
158+
159+
// Mock to return the mint data or the validation state account data
160+
async fn mock_fetch_account_data_fn(address: Pubkey) -> AccountDataResult {
161+
if address == MINT_PUBKEY {
162+
let mint_len =
163+
ExtensionType::try_calculate_account_len::<Mint>(&[ExtensionType::TransferHook])
164+
.unwrap();
165+
let mut data = vec![0u8; mint_len];
166+
let mut mint = StateWithExtensionsMut::<Mint>::unpack_uninitialized(&mut data).unwrap();
167+
168+
let extension = mint.init_extension::<TransferHook>(true).unwrap();
169+
extension.program_id =
170+
OptionalNonZeroPubkey::try_from(Some(TRANSFER_HOOK_PROGRAM_ID)).unwrap();
171+
172+
mint.base.mint_authority = COption::Some(Pubkey::new_unique());
173+
mint.base.decimals = DECIMALS;
174+
mint.base.is_initialized = true;
175+
mint.base.freeze_authority = COption::None;
176+
mint.pack_base();
177+
mint.init_account_type().unwrap();
178+
179+
Ok(Some(data))
180+
} else if address
181+
== get_extra_account_metas_address(&MINT_PUBKEY, &TRANSFER_HOOK_PROGRAM_ID)
182+
{
183+
let extra_metas = vec![
184+
ExtraAccountMeta::new_with_pubkey(&EXTRA_META_1, true, false).unwrap(),
185+
ExtraAccountMeta::new_with_pubkey(&EXTRA_META_2, true, false).unwrap(),
186+
ExtraAccountMeta::new_with_seeds(
187+
&[
188+
Seed::AccountKey { index: 0 }, // source
189+
Seed::AccountKey { index: 2 }, // destination
190+
Seed::AccountKey { index: 4 }, // validation state
191+
],
192+
false,
193+
true,
194+
)
195+
.unwrap(),
196+
ExtraAccountMeta::new_with_seeds(
197+
&[
198+
Seed::InstructionData {
199+
index: 8,
200+
length: 8,
201+
}, // amount
202+
Seed::AccountKey { index: 2 }, // destination
203+
Seed::AccountKey { index: 5 }, // extra meta 1
204+
Seed::AccountKey { index: 7 }, // extra meta 3 (PDA)
205+
],
206+
false,
207+
true,
208+
)
209+
.unwrap(),
210+
];
211+
let account_size = ExtraAccountMetaList::size_of(extra_metas.len()).unwrap();
212+
let mut data = vec![0u8; account_size];
213+
ExtraAccountMetaList::init::<ExecuteInstruction>(&mut data, &extra_metas)?;
214+
Ok(Some(data))
215+
} else {
216+
Ok(None)
217+
}
218+
}
219+
220+
#[tokio::test]
221+
async fn test_create_transfer_checked_instruction_with_extra_metas() {
222+
let source = Pubkey::new_unique();
223+
let destination = Pubkey::new_unique();
224+
let authority = Pubkey::new_unique();
225+
let amount = 100u64;
226+
227+
let validate_state_pubkey =
228+
get_extra_account_metas_address(&MINT_PUBKEY, &TRANSFER_HOOK_PROGRAM_ID);
229+
let extra_meta_3_pubkey = Pubkey::find_program_address(
230+
&[
231+
source.as_ref(),
232+
destination.as_ref(),
233+
validate_state_pubkey.as_ref(),
234+
],
235+
&TRANSFER_HOOK_PROGRAM_ID,
236+
)
237+
.0;
238+
let extra_meta_4_pubkey = Pubkey::find_program_address(
239+
&[
240+
amount.to_le_bytes().as_ref(),
241+
destination.as_ref(),
242+
EXTRA_META_1.as_ref(),
243+
extra_meta_3_pubkey.as_ref(),
244+
],
245+
&TRANSFER_HOOK_PROGRAM_ID,
246+
)
247+
.0;
248+
249+
let instruction = create_transfer_checked_instruction_with_extra_metas(
250+
&crate::id(),
251+
&source,
252+
&MINT_PUBKEY,
253+
&destination,
254+
&authority,
255+
&[],
256+
amount,
257+
DECIMALS,
258+
mock_fetch_account_data_fn,
259+
)
260+
.await
261+
.unwrap();
262+
263+
let check_metas = [
264+
AccountMeta::new(source, false),
265+
AccountMeta::new_readonly(MINT_PUBKEY, false),
266+
AccountMeta::new(destination, false),
267+
AccountMeta::new_readonly(authority, true),
268+
AccountMeta::new_readonly(EXTRA_META_1, true),
269+
AccountMeta::new_readonly(EXTRA_META_2, true),
270+
AccountMeta::new(extra_meta_3_pubkey, false),
271+
AccountMeta::new(extra_meta_4_pubkey, false),
272+
AccountMeta::new_readonly(TRANSFER_HOOK_PROGRAM_ID, false),
273+
AccountMeta::new_readonly(validate_state_pubkey, false),
274+
];
275+
276+
assert_eq!(instruction.accounts, check_metas);
277+
278+
// With additional signers
279+
let signer_1 = Pubkey::new_unique();
280+
let signer_2 = Pubkey::new_unique();
281+
let signer_3 = Pubkey::new_unique();
282+
283+
let instruction = create_transfer_checked_instruction_with_extra_metas(
284+
&crate::id(),
285+
&source,
286+
&MINT_PUBKEY,
287+
&destination,
288+
&authority,
289+
&[&signer_1, &signer_2, &signer_3],
290+
amount,
291+
DECIMALS,
292+
mock_fetch_account_data_fn,
293+
)
294+
.await
295+
.unwrap();
296+
297+
let check_metas = [
298+
AccountMeta::new(source, false),
299+
AccountMeta::new_readonly(MINT_PUBKEY, false),
300+
AccountMeta::new(destination, false),
301+
AccountMeta::new_readonly(authority, false), // False because of additional signers
302+
AccountMeta::new_readonly(signer_1, true),
303+
AccountMeta::new_readonly(signer_2, true),
304+
AccountMeta::new_readonly(signer_3, true),
305+
AccountMeta::new_readonly(EXTRA_META_1, true),
306+
AccountMeta::new_readonly(EXTRA_META_2, true),
307+
AccountMeta::new(extra_meta_3_pubkey, false),
308+
AccountMeta::new(extra_meta_4_pubkey, false),
309+
AccountMeta::new_readonly(TRANSFER_HOOK_PROGRAM_ID, false),
310+
AccountMeta::new_readonly(validate_state_pubkey, false),
311+
];
312+
313+
assert_eq!(instruction.accounts, check_metas);
314+
}
315+
}

0 commit comments

Comments
 (0)