Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ license = "WTFPL"

[dependencies]
solana-program = "1.4.6"
spl-token = { version = "3.0.0", features = ["no-entrypoint"]}

[features]
test-bpf = []
Expand Down
181 changes: 168 additions & 13 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
use solana_program::{
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, info, pubkey::Pubkey,
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
info,
program::{invoke, invoke_signed},
program_error::ProgramError,
program_pack::Pack,
pubkey::Pubkey,
rent::Rent,
system_instruction,
sysvar::Sysvar,
};

entrypoint!(process_instruction);
Expand All @@ -14,39 +24,184 @@ fn process_instruction(
accounts.len(),
instruction_data
));
Ok(())

let account_info_iter = &mut accounts.iter();
let program_token_info = next_account_info(account_info_iter)?;
let (program_token_address, program_token_bump_seed) =
Pubkey::find_program_address(&[br"program-token"], program_id);

if program_token_address != *program_token_info.key {
info!("Error: program token address derivation mismatch");
return Err(ProgramError::InvalidArgument);
}

let program_token_signer_seeds: &[&[_]] = &[br"program-token", &[program_token_bump_seed]];

match instruction_data.get(0) {
Some(0) => {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the create instruction is currently unprivileged, meaning that anybody can grief you by creating an arbitrary token at the fixed program address. In the real world, some authority should be checked before creating the token account

info!("Create program token account...");
let funder_info = next_account_info(account_info_iter)?;
let mint_info = next_account_info(account_info_iter)?;
let system_program_info = next_account_info(account_info_iter)?;
let spl_token_program_info = next_account_info(account_info_iter)?;
let rent_sysvar_info = next_account_info(account_info_iter)?;

let rent = &Rent::from_account_info(rent_sysvar_info)?;

invoke_signed(
&system_instruction::create_account(
Copy link

@wilbarnes wilbarnes Nov 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i noticed you used system_instruction::create_account() here, is that any different than using pubkey::create_program_address() https://github.com/solana-labs/solana/blob/89b474e192ea2f4b2274ad26cbc47443d5767571/sdk/program/src/pubkey.rs#L135 ?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, very different. Sadly the names are close enough that I can see where confusion can arise!

system_instruction::create_account() - this is transaction instruction implemented by the system program that when executed allocates a new account on the chain based on the given parameters. The address that the account is allocated at is also provided as an input to this instruction

pubkey::create_program_address() - a local function that returns an account address owned by the specified program, assuming the seeds are valid. This is just an address, a 32 byte number.

Copy link

@wilbarnes wilbarnes Nov 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, and i presume you chose to use Pubkey::find_program_address() instead of Pubkey::create_program_address(), because as you mentioned above in the initial comments we have a constant address in this program for users to deposit SPL tokens

i'll try it myself, but it seems to me that i can also just call the Pubkey::find_program_address(&[br"program-token2"], program_id); and it'll make a second associated account users can send same tokens to

then, i can use a totally different program_id for a whole different SPL token in this same program and be able to intake 2 SPL tokens: Pubkey::find_program_address(&[br"program-token"], program_id2); right?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and after creating them, this program can do invoke_signed on them and send them wherever they want

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that's right.

i can use a totally different program_id for a whole different SPL token in this same program and be able to intake 2 SPL tokens: Pubkey::find_program_address(&[br"program-token"], program_id2); right?

Alternatively you use the same program, but adding more seeds to find_program_address:

Pubkey::find_program_address(&[br"program-token", some_other_seed], program_id);

Here's an example to check out: https://github.com/solana-labs/solana-program-library/blob/487ad2d2d7cb0b8a58f02a156a659e6c8a74f354/associated-token-account/program/src/lib.rs#L24-L32

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you, this is very helpful

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is program_id in the Pubkey::find_program_address() function supposed to be the SPL token address that i've created (not the spl_token::id()? for example, I've created an SPL token, and i want to create an associated account for that token for my program (same as the one above)

funder_info.key,
program_token_info.key,
1.max(rent.minimum_balance(spl_token::state::Account::get_packed_len())),
spl_token::state::Account::get_packed_len() as u64,
&spl_token::id(),
),
&[
funder_info.clone(),
program_token_info.clone(),
system_program_info.clone(),
],
&[&program_token_signer_seeds],
)?;

info!("Initializing program token account");
invoke(
&spl_token::instruction::initialize_account(
&spl_token::id(),
program_token_info.key,
mint_info.key,
program_token_info.key, // token owner is also `program_token` address
)?,
&[
program_token_info.clone(),
spl_token_program_info.clone(),
rent_sysvar_info.clone(),
mint_info.clone(),
],
)?;
Ok(())
}
Some(1) => {
info!("Close program token account...");
let funder_info = next_account_info(account_info_iter)?;
let spl_token_program_info = next_account_info(account_info_iter)?;

invoke_signed(
&spl_token::instruction::close_account(
&spl_token::id(),
program_token_info.key,
funder_info.key,
program_token_info.key, // token owner is also `program_token` address
&[],
)
.expect("close_account"),
&[
funder_info.clone(),
spl_token_program_info.clone(),
program_token_info.clone(),
],
&[&program_token_signer_seeds],
)
}
_ => {
info!("Error: Unsupported instruction");
Err(ProgramError::InvalidInstructionData)
}
}
}

#[cfg(test)]
mod test {
#![cfg(feature = "test-bpf")]

use super::*;
use assert_matches::*;
use solana_program::instruction::{AccountMeta, Instruction};
use solana_program::{
instruction::{AccountMeta, Instruction},
sysvar,
};
use solana_program_test::*;
use solana_sdk::{signature::Signer, transaction::Transaction};

fn program_test(program_id: Pubkey) -> ProgramTest {
let mut pc = ProgramTest::new(
"bpf_program_template",
program_id,
processor!(process_instruction),
);

// Add SPL Token program
pc.add_program(
"spl_token",
spl_token::id(),
Comment on lines +134 to +136

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm still confused by this pc.add_program() function, it seems like it's not even being used. i've browsed around through the repo and i can't really find precisely what spl_token::id() is, though i'm pretty sure it's just the spl_token's account address that we've added to ProgramTest

what is spl_token::id() here? and further, how is that the program itself and also the tests (see line 56) can simply use spl_token::id()?

going back to my confusion of pc.add_program()... on line 145, you've just called let program_id = Pubkey::new_unique(); that returns a unique account pubkey, shouldn't the spl_token program we added to the program test be the program_id we're using in our tests?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spl_token::id() is actually this: https://github.com/solana-labs/solana-program-library/blob/master/token/program/src/lib.rs#L29

going back to my confusion of pc.add_program()... on line 145, you've just called let program_id = Pubkey::new_unique(); that returns a unique account pubkey, shouldn't the spl_token program we added to the program test be the program_id we're using in our tests?

let program_id = Pubkey::new_unique() is the program id of bpf_program_template, the actual program that was written here. spl_token::id() is the program id of the SPL Token program, which is invoked by bpf_program_template

processor!(spl_token::processor::Processor::process),
);

pc
}

#[tokio::test]
async fn test_transaction() {
async fn test_create_then_close() {
let program_id = Pubkey::new_unique();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is a program_id created here, when we have the pc.add_program() call in fn program_test(), which should just create a mock spl_token to use?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i was just interpreting it incorrectly, Pubkey::new_unique() creates the program_id we then instantiante

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the program_id for the "bpf-program-template program, passed in via ProgramTest::new(..). It's just a unnamed, unique id here because the actual value doesn't matter for testing purposes -- it just needs to be something. On a real cluster, this would be the value returned by solana deploy` when the program is deployed

let (mut banks_client, payer, recent_blockhash) = program_test(program_id).start().await;

let (mut banks_client, payer, recent_blockhash) =
ProgramTest::new("bpf_program_template", program_id, processor!(process_instruction))
.start()
.await;
let program_token_address =
Pubkey::find_program_address(&[br"program-token"], &program_id).0;

// Create the program-owned token account
let mut transaction = Transaction::new_with_payer(
&[Instruction {
program_id,
accounts: vec![AccountMeta::new(payer.pubkey(), false)],
data: vec![1, 2, 3],
accounts: vec![
AccountMeta::new(program_token_address, false),
AccountMeta::new(payer.pubkey(), true),
AccountMeta::new_readonly(spl_token::native_mint::id(), false),
AccountMeta::new_readonly(solana_program::system_program::id(), false),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
],
data: vec![0],
}],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer], recent_blockhash);
assert_matches!(banks_client.process_transaction(transaction).await, Ok(()));

assert_matches!(
banks_client.process_transaction(transaction).await,
Ok(())
// Fetch the program-owned token account and confirm it now exists
let program_token_account = banks_client
.get_account(program_token_address)
.await
.expect("success")
.expect("some account");
let program_token_account =
spl_token::state::Account::unpack_from_slice(&program_token_account.data)
.expect("unpack success");
assert_eq!(program_token_account.mint, spl_token::native_mint::id());
assert_eq!(program_token_account.owner, program_token_address);

// Close the the program-owned token account
let mut transaction = Transaction::new_with_payer(
&[Instruction {
program_id,
accounts: vec![
AccountMeta::new(program_token_address, false),
AccountMeta::new(payer.pubkey(), true),
AccountMeta::new_readonly(spl_token::id(), false),
],
data: vec![1],
}],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer], recent_blockhash);
assert_matches!(banks_client.process_transaction(transaction).await, Ok(()));

// Fetch the program-owned token account and confirm it no longer now exists
assert_eq!(
banks_client
.get_account(program_token_address)
.await
.expect("success"),
None
);
}
}