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

Commit 9af36d8

Browse files
authored
transfer-hook-example: Only allow one mint to initialize (#6812)
* transfer-hook-example: Only allow one mint * Add a crate feature to fix downstream tests easily
1 parent 9f8d4f3 commit 9af36d8

File tree

8 files changed

+141
-46
lines changed

8 files changed

+141
-46
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/js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"clean": "shx rm -rf lib **/*.tsbuildinfo || true",
3434
"build": "tsc --build --verbose tsconfig.all.json",
3535
"postbuild": "shx echo '{ \"type\": \"commonjs\" }' > lib/cjs/package.json",
36-
"build:program": "cargo build-sbf --manifest-path=../program/Cargo.toml && cargo build-sbf --manifest-path=../program-2022/Cargo.toml && cargo build-sbf --manifest-path=../../associated-token-account/program/Cargo.toml && cargo build-sbf --manifest-path=../transfer-hook/example/Cargo.toml",
36+
"build:program": "cargo build-sbf --manifest-path=../program/Cargo.toml && cargo build-sbf --manifest-path=../program-2022/Cargo.toml && cargo build-sbf --manifest-path=../../associated-token-account/program/Cargo.toml && cargo build-sbf --no-default-features --manifest-path=../transfer-hook/example/Cargo.toml",
3737
"watch": "tsc --build --verbose --watch tsconfig.all.json",
3838
"release": "npm run clean && npm run build",
3939
"fmt": "prettier --write '{*,**/*}.{ts,tsx,js,jsx,json}'",

token/transfer-hook/cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ serde_yaml = "0.9.34"
3030
solana-test-validator = ">=1.18.11,<=2"
3131
spl-token-2022 = { version = "3.0.2", path = "../../program-2022", features = ["no-entrypoint"] }
3232
spl-token-client = { version = "0.10.0", path = "../../client" }
33+
spl-transfer-hook-example = { version = "0.6.0", path = "../example" }
3334

3435
[[bin]]
3536
name = "spl-transfer-hook"

token/transfer-hook/cli/src/main.rs

Lines changed: 48 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
263263
Additional accounts with known fixed addresses can be passed at the command line in the format "<PUBKEY>:<ROLE>". The role must be "readonly", "writable". "readonlySigner", or "writableSigner".
264264
265265
Additional accounts requiring seed configurations can be defined in a configuration file using either JSON or YAML. The format is as follows:
266-
266+
267267
```json
268268
{
269269
"extraMetas": [
@@ -347,7 +347,7 @@ extraMetas:
347347
Additional accounts with known fixed addresses can be passed at the command line in the format "<PUBKEY>:<ROLE>". The role must be "readonly", "writable". "readonlySigner", or "writableSigner".
348348
349349
Additional accounts requiring seed configurations can be defined in a configuration file using either JSON or YAML. The format is as follows:
350-
350+
351351
```json
352352
{
353353
"extraMetas": [
@@ -551,19 +551,27 @@ extraMetas:
551551
mod test {
552552
use {
553553
super::*,
554-
solana_sdk::{bpf_loader_upgradeable, instruction::AccountMeta, signer::keypair::Keypair},
554+
solana_sdk::{
555+
account::Account, bpf_loader_upgradeable, instruction::AccountMeta,
556+
program_option::COption, signer::keypair::Keypair,
557+
},
555558
solana_test_validator::{TestValidator, TestValidatorGenesis, UpgradeableProgramInfo},
559+
spl_token_2022::{
560+
extension::{ExtensionType, StateWithExtensionsMut},
561+
state::Mint,
562+
},
556563
spl_token_client::{
557-
client::{
558-
ProgramClient, ProgramRpcClient, ProgramRpcClientSendTransaction, SendTransaction,
559-
SimulateTransaction,
560-
},
564+
client::{ProgramRpcClient, ProgramRpcClientSendTransaction},
561565
token::Token,
562566
},
563567
std::{path::PathBuf, sync::Arc},
564568
};
565569

566-
async fn new_validator_for_test(program_id: Pubkey) -> (TestValidator, Keypair) {
570+
async fn new_validator_for_test(
571+
program_id: Pubkey,
572+
mint_authority: &Pubkey,
573+
decimals: u8,
574+
) -> (TestValidator, Keypair) {
567575
solana_logger::setup();
568576
let mut test_validator_genesis = TestValidatorGenesis::default();
569577
test_validator_genesis.add_upgradeable_programs_with_path(&[UpgradeableProgramInfo {
@@ -572,54 +580,55 @@ mod test {
572580
program_path: PathBuf::from("../../../target/deploy/spl_transfer_hook_example.so"),
573581
upgrade_authority: Pubkey::new_unique(),
574582
}]);
575-
test_validator_genesis.start_async().await
576-
}
577583

578-
async fn setup_mint<T: SendTransaction + SimulateTransaction>(
579-
program_id: &Pubkey,
580-
mint_authority: &Pubkey,
581-
decimals: u8,
582-
payer: Arc<dyn Signer>,
583-
client: Arc<dyn ProgramClient<T>>,
584-
) -> Token<T> {
585-
let mint_account = Keypair::new();
586-
let token = Token::new(
587-
client,
588-
program_id,
589-
&mint_account.pubkey(),
590-
Some(decimals),
591-
payer,
584+
let mint_size = ExtensionType::try_calculate_account_len::<Mint>(&[]).unwrap();
585+
let mut mint_data = vec![0; mint_size];
586+
let mut state =
587+
StateWithExtensionsMut::<Mint>::unpack_uninitialized(&mut mint_data).unwrap();
588+
let token_amount = 1_000_000_000_000;
589+
state.base = Mint {
590+
mint_authority: COption::Some(*mint_authority),
591+
supply: token_amount,
592+
decimals,
593+
is_initialized: true,
594+
freeze_authority: COption::None,
595+
};
596+
state.pack_base();
597+
test_validator_genesis.add_account(
598+
spl_transfer_hook_example::mint::id(),
599+
Account {
600+
lamports: 1_000_000_000,
601+
data: mint_data,
602+
owner: spl_token_2022::id(),
603+
..Account::default()
604+
}
605+
.into(),
592606
);
593-
token
594-
.create_mint(mint_authority, None, vec![], &[&mint_account])
595-
.await
596-
.unwrap();
597-
token
607+
test_validator_genesis.start_async().await
598608
}
599609

600610
#[tokio::test]
601611
async fn test_create() {
602612
let program_id = Pubkey::new_unique();
603613

604-
let (test_validator, payer) = new_validator_for_test(program_id).await;
614+
let decimals = 2;
615+
let mint_authority = Keypair::new();
616+
let (test_validator, payer) =
617+
new_validator_for_test(program_id, &mint_authority.pubkey(), decimals).await;
605618
let payer: Arc<dyn Signer> = Arc::new(payer);
606619
let rpc_client = Arc::new(test_validator.get_async_rpc_client());
607620
let client = Arc::new(ProgramRpcClient::new(
608621
rpc_client.clone(),
609622
ProgramRpcClientSendTransaction,
610623
));
611624

612-
let mint_authority = Keypair::new();
613-
let decimals = 2;
614-
615-
let token = setup_mint(
625+
let token = Token::new(
626+
client.clone(),
616627
&spl_token_2022::id(),
617-
&mint_authority.pubkey(),
618-
decimals,
628+
&spl_transfer_hook_example::mint::id(),
629+
Some(decimals),
619630
payer.clone(),
620-
client.clone(),
621-
)
622-
.await;
631+
);
623632

624633
let required_address = Pubkey::new_unique();
625634
let accounts = vec![AccountMeta::new_readonly(required_address, false)];

token/transfer-hook/example/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ license = "Apache-2.0"
88
edition = "2021"
99

1010
[features]
11+
default = ["forbid-additional-mints"]
1112
no-entrypoint = []
1213
test-sbf = []
14+
forbid-additional-mints = []
1315

1416
[dependencies]
1517
arrayref = "0.3.7"

token/transfer-hook/example/src/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,15 @@ mod entrypoint;
1616
// Export current sdk types for downstream users building with a different sdk
1717
// version
1818
pub use solana_program;
19+
20+
/// Place the mint id that you want to target with your transfer hook program.
21+
/// Any other mint will fail to initialize, protecting the transfer hook program
22+
/// from rogue mints trying to get access to accounts.
23+
///
24+
/// There are many situations where it's reasonable to support multiple mints
25+
/// with one transfer-hook program, but because it's easy to make something
26+
/// unsafe, this simple example implementation only allows for one mint.
27+
#[cfg(feature = "forbid-additional-mints")]
28+
pub mod mint {
29+
solana_program::declare_id!("Mint111111111111111111111111111111111111111");
30+
}

token/transfer-hook/example/src/processor.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ pub fn process_initialize_extra_account_meta_list(
8888
let authority_info = next_account_info(account_info_iter)?;
8989
let _system_program_info = next_account_info(account_info_iter)?;
9090

91+
// check that the one mint we want to target is trying to create extra
92+
// account metas
93+
#[cfg(feature = "forbid-additional-mints")]
94+
if *mint_info.key != crate::mint::id() {
95+
return Err(ProgramError::InvalidArgument);
96+
}
97+
9198
// check that the mint authority is valid without fully deserializing
9299
let mint_data = mint_info.try_borrow_data()?;
93100
let mint = StateWithExtensions::<Mint>::unpack(&mint_data)?;

token/transfer-hook/example/tests/functional.rs

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ async fn success_execute() {
142142

143143
let token_program_id = spl_token_2022::id();
144144
let wallet = Keypair::new();
145-
let mint_address = Pubkey::new_unique();
145+
let mint_address = spl_transfer_hook_example::mint::id();
146146
let mint_authority = Keypair::new();
147147
let mint_authority_pubkey = mint_authority.pubkey();
148148
let source = Pubkey::new_unique();
@@ -439,7 +439,7 @@ async fn fail_incorrect_derivation() {
439439

440440
let token_program_id = spl_token_2022::id();
441441
let wallet = Keypair::new();
442-
let mint_address = Pubkey::new_unique();
442+
let mint_address = spl_transfer_hook_example::mint::id();
443443
let mint_authority = Keypair::new();
444444
let mint_authority_pubkey = mint_authority.pubkey();
445445
let source = Pubkey::new_unique();
@@ -495,6 +495,69 @@ async fn fail_incorrect_derivation() {
495495
);
496496
}
497497

498+
#[tokio::test]
499+
async fn fail_incorrect_mint() {
500+
let program_id = Pubkey::new_unique();
501+
let mut program_test = setup(&program_id);
502+
503+
let token_program_id = spl_token_2022::id();
504+
let wallet = Keypair::new();
505+
// wrong mint, only `spl_transfer_hook_example::mint::id()` allowed
506+
let mint_address = Pubkey::new_unique();
507+
let mint_authority = Keypair::new();
508+
let mint_authority_pubkey = mint_authority.pubkey();
509+
let source = Pubkey::new_unique();
510+
let destination = Pubkey::new_unique();
511+
let decimals = 2;
512+
setup_token_accounts(
513+
&mut program_test,
514+
&token_program_id,
515+
&mint_address,
516+
&mint_authority_pubkey,
517+
&source,
518+
&destination,
519+
&wallet.pubkey(),
520+
decimals,
521+
true,
522+
);
523+
524+
let extra_account_metas = get_extra_account_metas_address(&mint_address, &program_id);
525+
526+
let mut context = program_test.start_with_context().await;
527+
let rent = context.banks_client.get_rent().await.unwrap();
528+
let rent_lamports = rent.minimum_balance(ExtraAccountMetaList::size_of(0).unwrap());
529+
530+
let transaction = Transaction::new_signed_with_payer(
531+
&[
532+
system_instruction::transfer(
533+
&context.payer.pubkey(),
534+
&extra_account_metas,
535+
rent_lamports,
536+
),
537+
initialize_extra_account_meta_list(
538+
&program_id,
539+
&extra_account_metas,
540+
&mint_address,
541+
&mint_authority_pubkey,
542+
&[],
543+
),
544+
],
545+
Some(&context.payer.pubkey()),
546+
&[&context.payer, &mint_authority],
547+
context.last_blockhash,
548+
);
549+
let error = context
550+
.banks_client
551+
.process_transaction(transaction)
552+
.await
553+
.unwrap_err()
554+
.unwrap();
555+
assert_eq!(
556+
error,
557+
TransactionError::InstructionError(1, InstructionError::InvalidArgument)
558+
);
559+
}
560+
498561
/// Test program to CPI into default transfer-hook-interface program
499562
pub fn process_instruction(
500563
_program_id: &Pubkey,
@@ -530,7 +593,7 @@ async fn success_on_chain_invoke() {
530593

531594
let token_program_id = spl_token_2022::id();
532595
let wallet = Keypair::new();
533-
let mint_address = Pubkey::new_unique();
596+
let mint_address = spl_transfer_hook_example::mint::id();
534597
let mint_authority = Keypair::new();
535598
let mint_authority_pubkey = mint_authority.pubkey();
536599
let source = Pubkey::new_unique();
@@ -673,7 +736,7 @@ async fn fail_without_transferring_flag() {
673736

674737
let token_program_id = spl_token_2022::id();
675738
let wallet = Keypair::new();
676-
let mint_address = Pubkey::new_unique();
739+
let mint_address = spl_transfer_hook_example::mint::id();
677740
let mint_authority = Keypair::new();
678741
let mint_authority_pubkey = mint_authority.pubkey();
679742
let source = Pubkey::new_unique();
@@ -767,7 +830,7 @@ async fn success_on_chain_invoke_with_updated_extra_account_metas() {
767830

768831
let token_program_id = spl_token_2022::id();
769832
let wallet = Keypair::new();
770-
let mint_address = Pubkey::new_unique();
833+
let mint_address = spl_transfer_hook_example::mint::id();
771834
let mint_authority = Keypair::new();
772835
let mint_authority_pubkey = mint_authority.pubkey();
773836
let source = Pubkey::new_unique();
@@ -970,7 +1033,7 @@ async fn success_execute_with_updated_extra_account_metas() {
9701033

9711034
let token_program_id = spl_token_2022::id();
9721035
let wallet = Keypair::new();
973-
let mint_address = Pubkey::new_unique();
1036+
let mint_address = spl_transfer_hook_example::mint::id();
9741037
let mint_authority = Keypair::new();
9751038
let mint_authority_pubkey = mint_authority.pubkey();
9761039
let source = Pubkey::new_unique();

0 commit comments

Comments
 (0)