diff --git a/tokens/token-2022/cpi-guard/steel/.gitignore b/tokens/token-2022/cpi-guard/steel/.gitignore new file mode 100644 index 00000000..052739db --- /dev/null +++ b/tokens/token-2022/cpi-guard/steel/.gitignore @@ -0,0 +1,2 @@ +target +test-ledger diff --git a/tokens/token-2022/cpi-guard/steel/Cargo.toml b/tokens/token-2022/cpi-guard/steel/Cargo.toml new file mode 100644 index 00000000..fc9d5f88 --- /dev/null +++ b/tokens/token-2022/cpi-guard/steel/Cargo.toml @@ -0,0 +1,22 @@ +[workspace] +resolver = "2" +members = ["api", "program"] + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +homepage = "" +documentation = "" +repository = "" +readme = "./README.md" +keywords = ["solana"] + +[workspace.dependencies] +steel-api = { path = "./api", version = "0.1.0" } +bytemuck = "1.14" +num_enum = "0.7" +solana-program = "2.1" +steel = "3.0" +thiserror = "1.0" +spl-token-2022 = {version = "7.0.0", features = ["no-entrypoint"]} diff --git a/tokens/token-2022/cpi-guard/steel/README.md b/tokens/token-2022/cpi-guard/steel/README.md new file mode 100644 index 00000000..cfc508fd --- /dev/null +++ b/tokens/token-2022/cpi-guard/steel/README.md @@ -0,0 +1,28 @@ +# Steel + +**Steel** is a ... + +## API +- [`Consts`](api/src/consts.rs) – Program constants. +- [`Error`](api/src/error.rs) – Custom program errors. +- [`Event`](api/src/event.rs) – Custom program events. +- [`Instruction`](api/src/instruction.rs) – Declared instructions. + +## Instructions +- [`Add`](program/src/add.rs) – Add ... +- [`Initialize`](program/src/initialize.rs) – Initialize ... + +## State +- [`Counter`](api/src/state/counter.rs) – Counter ... + +## Get started + +Compile your program: +```sh +steel build +``` + +Run unit and integration tests: +```sh +steel test +``` diff --git a/tokens/token-2022/cpi-guard/steel/api/Cargo.toml b/tokens/token-2022/cpi-guard/steel/api/Cargo.toml new file mode 100644 index 00000000..e514258e --- /dev/null +++ b/tokens/token-2022/cpi-guard/steel/api/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "steel-api" +description = "API for interacting with the Steel program" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true + +[dependencies] +bytemuck.workspace = true +num_enum.workspace = true +solana-program.workspace = true +steel.workspace = true +thiserror.workspace = true +spl-token-2022.workspace = true diff --git a/tokens/token-2022/cpi-guard/steel/api/src/error.rs b/tokens/token-2022/cpi-guard/steel/api/src/error.rs new file mode 100644 index 00000000..96e84da4 --- /dev/null +++ b/tokens/token-2022/cpi-guard/steel/api/src/error.rs @@ -0,0 +1,10 @@ +use steel::*; + +#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, IntoPrimitive)] +#[repr(u32)] +pub enum SteelError { + #[error("This is a dummy error")] + Dummy = 0, +} + +error!(SteelError); diff --git a/tokens/token-2022/cpi-guard/steel/api/src/instruction.rs b/tokens/token-2022/cpi-guard/steel/api/src/instruction.rs new file mode 100644 index 00000000..02ac6272 --- /dev/null +++ b/tokens/token-2022/cpi-guard/steel/api/src/instruction.rs @@ -0,0 +1,13 @@ +use steel::*; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] +pub enum SteelInstruction { + CpiBurn = 0, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct CpiBurn {} + +instruction!(SteelInstruction, CpiBurn); diff --git a/tokens/token-2022/cpi-guard/steel/api/src/lib.rs b/tokens/token-2022/cpi-guard/steel/api/src/lib.rs new file mode 100644 index 00000000..2c146dac --- /dev/null +++ b/tokens/token-2022/cpi-guard/steel/api/src/lib.rs @@ -0,0 +1,14 @@ +pub mod error; +pub mod instruction; +pub mod sdk; + +pub mod prelude { + pub use crate::error::*; + pub use crate::instruction::*; + pub use crate::sdk::*; +} + +use steel::*; + +// TODO Set program id +declare_id!("z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35"); diff --git a/tokens/token-2022/cpi-guard/steel/api/src/sdk.rs b/tokens/token-2022/cpi-guard/steel/api/src/sdk.rs new file mode 100644 index 00000000..0a51d4c6 --- /dev/null +++ b/tokens/token-2022/cpi-guard/steel/api/src/sdk.rs @@ -0,0 +1,17 @@ +use steel::*; + +use crate::prelude::*; + +pub fn cpi_burn(signer: Pubkey, mint: Pubkey, recipient_token_account: Pubkey) -> Instruction { + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(signer, true), + AccountMeta::new(mint, false), + AccountMeta::new(recipient_token_account, false), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(spl_token_2022::ID, false), + ], + data: CpiBurn {}.to_bytes(), + } +} diff --git a/tokens/token-2022/cpi-guard/steel/program/Cargo.toml b/tokens/token-2022/cpi-guard/steel/program/Cargo.toml new file mode 100644 index 00000000..11f2289d --- /dev/null +++ b/tokens/token-2022/cpi-guard/steel/program/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "steel-program" +description = "" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +steel-api.workspace = true +solana-program.workspace = true +steel.workspace = true +spl-token-2022.workspace = true + +[dev-dependencies] +base64 = "0.21" +rand = "0.8.5" +solana-program-test = "2.1" +solana-sdk = "2.1" +tokio = { version = "1.35", features = ["full"] } diff --git a/tokens/token-2022/cpi-guard/steel/program/src/cpi_burn.rs b/tokens/token-2022/cpi-guard/steel/program/src/cpi_burn.rs new file mode 100644 index 00000000..417e8a76 --- /dev/null +++ b/tokens/token-2022/cpi-guard/steel/program/src/cpi_burn.rs @@ -0,0 +1,54 @@ +use solana_program::{msg, program::invoke}; +use spl_token_2022::instruction::{burn, mint_to}; +use steel::*; + +pub fn process_cpi_burn(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { + // Load accounts. + let [signer_info, mint_info, recipient_token_account_info, system_program, token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + //Validation + signer_info.is_signer()?; + recipient_token_account_info.is_writable()?; + token_program.is_program(&spl_token_2022::ID)?; + system_program.is_program(&system_program::ID)?; + + invoke( + &mint_to( + token_program.key, + mint_info.key, + recipient_token_account_info.key, + signer_info.key, + &[], + 1000, + )?, + &[ + mint_info.clone(), + recipient_token_account_info.clone(), + signer_info.clone(), + ], + )?; + + invoke( + &burn( + token_program.key, + recipient_token_account_info.key, + mint_info.key, + signer_info.key, + &[], + 100, + )?, + &[ + recipient_token_account_info.clone(), + mint_info.clone(), + signer_info.clone(), + ], + )?; + + msg!("Cpi Guard Extension Test: Burn."); + + Ok(()) +} diff --git a/tokens/token-2022/cpi-guard/steel/program/src/lib.rs b/tokens/token-2022/cpi-guard/steel/program/src/lib.rs new file mode 100644 index 00000000..7958c6be --- /dev/null +++ b/tokens/token-2022/cpi-guard/steel/program/src/lib.rs @@ -0,0 +1,22 @@ +mod cpi_burn; + +use cpi_burn::*; + +use steel::*; +use steel_api::prelude::*; + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let (ix, data) = parse_instruction(&steel_api::ID, program_id, data)?; + + match ix { + SteelInstruction::CpiBurn => process_cpi_burn(accounts, data)?, + } + + Ok(()) +} + +entrypoint!(process_instruction); diff --git a/tokens/token-2022/cpi-guard/steel/program/tests/test.rs b/tokens/token-2022/cpi-guard/steel/program/tests/test.rs new file mode 100644 index 00000000..b3e59938 --- /dev/null +++ b/tokens/token-2022/cpi-guard/steel/program/tests/test.rs @@ -0,0 +1,133 @@ +use solana_program::{hash::Hash, program_pack::Pack}; +use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_sdk::{ + signature::Keypair, signer::Signer, system_instruction::create_account, + transaction::Transaction, +}; +use spl_token_2022::{ + extension::{ + cpi_guard::instruction::{disable_cpi_guard, enable_cpi_guard}, + ExtensionType, + }, + instruction::{initialize_account3, initialize_mint2}, + state::{Account, Mint}, +}; +use steel_api::prelude::*; + +async fn setup() -> (BanksClient, Keypair, Hash) { + let mut program_test = ProgramTest::new( + "steel_program", + steel_api::ID, + processor!(steel_program::process_instruction), + ); + program_test.prefer_bpf(true); + program_test.start().await +} + +#[tokio::test] +async fn run_test() { + // Setup test + let (mut banks, payer, blockhash) = setup().await; + + let mint = Keypair::new(); + let token_acc = Keypair::new(); + + //Setup Mint. + let create_mint_ix = create_account( + &payer.pubkey(), + &mint.pubkey(), + banks.get_rent().await.unwrap().minimum_balance(Mint::LEN), + Mint::LEN as u64, + &spl_token_2022::ID, + ); + let init_mint_ix = initialize_mint2( + &spl_token_2022::ID, + &mint.pubkey(), + &payer.pubkey(), + None, + 6, + ) + .unwrap(); + + //Setup Token Account + let space = + ExtensionType::try_calculate_account_len::(&[ExtensionType::CpiGuard]).unwrap(); + + let create_acc_ix = create_account( + &payer.pubkey(), + &token_acc.pubkey(), + banks.get_rent().await.unwrap().minimum_balance(space), + space as u64, + &spl_token_2022::ID, + ); + + let init_acc_ix = initialize_account3( + &spl_token_2022::ID, + &token_acc.pubkey(), + &mint.pubkey(), + &payer.pubkey(), + ) + .unwrap(); + + //Initialize Cpi Guard Extension + let enable_cpi_guard_ix = enable_cpi_guard( + &spl_token_2022::ID, + &token_acc.pubkey(), + &payer.pubkey(), + &[], + ) + .unwrap(); + + let tx = Transaction::new_signed_with_payer( + &[ + create_mint_ix, + init_mint_ix, + create_acc_ix, + init_acc_ix, + enable_cpi_guard_ix, + ], + Some(&payer.pubkey()), + &[&payer, &mint, &token_acc], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + //Try to burn tokens via cpi, should fail + let cpi_burn_ix = cpi_burn(payer.pubkey(), mint.pubkey(), token_acc.pubkey()); + + let tx = Transaction::new_signed_with_payer( + &[cpi_burn_ix], + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); + let res = banks.process_transaction(tx).await; + + let err_string = format!("{:?}", res); + + assert!( + err_string.contains("Custom(43)"), + "Expected TokenError::CpiGuardBurnBlocked(43) , got: {}", + err_string + ); + + //Disable CPI Guard and try burn again, should pass + let disable_cpi_ix = disable_cpi_guard( + &spl_token_2022::ID, + &token_acc.pubkey(), + &payer.pubkey(), + &[], + ) + .unwrap(); + let cpi_burn_ix = cpi_burn(payer.pubkey(), mint.pubkey(), token_acc.pubkey()); + + let tx = Transaction::new_signed_with_payer( + &[disable_cpi_ix, cpi_burn_ix], + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); +} diff --git a/tokens/token-2022/default-account-state/steel/.gitignore b/tokens/token-2022/default-account-state/steel/.gitignore new file mode 100644 index 00000000..052739db --- /dev/null +++ b/tokens/token-2022/default-account-state/steel/.gitignore @@ -0,0 +1,2 @@ +target +test-ledger diff --git a/tokens/token-2022/default-account-state/steel/Cargo.toml b/tokens/token-2022/default-account-state/steel/Cargo.toml new file mode 100644 index 00000000..8450ce0b --- /dev/null +++ b/tokens/token-2022/default-account-state/steel/Cargo.toml @@ -0,0 +1,23 @@ +[workspace] +resolver = "2" +members = ["api", "program"] + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +homepage = "" +documentation = "" +repository = "" +readme = "./README.md" +keywords = ["solana"] + +[workspace.dependencies] +steel-api = { path = "./api", version = "0.1.0" } +bytemuck = "1.14" +num_enum = "0.7" +solana-program = "2.1" +steel = "4.0" +thiserror = "1.0" +spl-token-2022 = {version = "7.0.0", features = ["no-entrypoint"]} + diff --git a/tokens/token-2022/default-account-state/steel/README.md b/tokens/token-2022/default-account-state/steel/README.md new file mode 100644 index 00000000..cfc508fd --- /dev/null +++ b/tokens/token-2022/default-account-state/steel/README.md @@ -0,0 +1,28 @@ +# Steel + +**Steel** is a ... + +## API +- [`Consts`](api/src/consts.rs) – Program constants. +- [`Error`](api/src/error.rs) – Custom program errors. +- [`Event`](api/src/event.rs) – Custom program events. +- [`Instruction`](api/src/instruction.rs) – Declared instructions. + +## Instructions +- [`Add`](program/src/add.rs) – Add ... +- [`Initialize`](program/src/initialize.rs) – Initialize ... + +## State +- [`Counter`](api/src/state/counter.rs) – Counter ... + +## Get started + +Compile your program: +```sh +steel build +``` + +Run unit and integration tests: +```sh +steel test +``` diff --git a/tokens/token-2022/default-account-state/steel/api/Cargo.toml b/tokens/token-2022/default-account-state/steel/api/Cargo.toml new file mode 100644 index 00000000..e514258e --- /dev/null +++ b/tokens/token-2022/default-account-state/steel/api/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "steel-api" +description = "API for interacting with the Steel program" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true + +[dependencies] +bytemuck.workspace = true +num_enum.workspace = true +solana-program.workspace = true +steel.workspace = true +thiserror.workspace = true +spl-token-2022.workspace = true diff --git a/tokens/token-2022/default-account-state/steel/api/src/error.rs b/tokens/token-2022/default-account-state/steel/api/src/error.rs new file mode 100644 index 00000000..96e84da4 --- /dev/null +++ b/tokens/token-2022/default-account-state/steel/api/src/error.rs @@ -0,0 +1,10 @@ +use steel::*; + +#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, IntoPrimitive)] +#[repr(u32)] +pub enum SteelError { + #[error("This is a dummy error")] + Dummy = 0, +} + +error!(SteelError); diff --git a/tokens/token-2022/default-account-state/steel/api/src/instruction.rs b/tokens/token-2022/default-account-state/steel/api/src/instruction.rs new file mode 100644 index 00000000..8ddb5a76 --- /dev/null +++ b/tokens/token-2022/default-account-state/steel/api/src/instruction.rs @@ -0,0 +1,19 @@ +use steel::*; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] +pub enum SteelInstruction { + Initialize = 0, + UpdateDefaultAccountState = 1, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct Initialize {} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct UpdateDefaultAccountState {} + +instruction!(SteelInstruction, Initialize); +instruction!(SteelInstruction, UpdateDefaultAccountState); diff --git a/tokens/token-2022/default-account-state/steel/api/src/lib.rs b/tokens/token-2022/default-account-state/steel/api/src/lib.rs new file mode 100644 index 00000000..2c146dac --- /dev/null +++ b/tokens/token-2022/default-account-state/steel/api/src/lib.rs @@ -0,0 +1,14 @@ +pub mod error; +pub mod instruction; +pub mod sdk; + +pub mod prelude { + pub use crate::error::*; + pub use crate::instruction::*; + pub use crate::sdk::*; +} + +use steel::*; + +// TODO Set program id +declare_id!("z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35"); diff --git a/tokens/token-2022/default-account-state/steel/api/src/sdk.rs b/tokens/token-2022/default-account-state/steel/api/src/sdk.rs new file mode 100644 index 00000000..38813aa2 --- /dev/null +++ b/tokens/token-2022/default-account-state/steel/api/src/sdk.rs @@ -0,0 +1,30 @@ +use steel::*; + +use crate::prelude::*; + +pub fn initialize(signer: Pubkey, mint: Pubkey) -> Instruction { + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(signer, true), + AccountMeta::new(mint, true), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(spl_token_2022::ID, false), + AccountMeta::new_readonly(sysvar::rent::ID, false), + ], + data: Initialize {}.to_bytes(), + } +} + +pub fn update_default_account_state(signer: Pubkey, mint: Pubkey) -> Instruction { + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(signer, true), + AccountMeta::new(mint, true), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(spl_token_2022::ID, false), + ], + data: UpdateDefaultAccountState {}.to_bytes(), + } +} diff --git a/tokens/token-2022/default-account-state/steel/program/Cargo.toml b/tokens/token-2022/default-account-state/steel/program/Cargo.toml new file mode 100644 index 00000000..1158b383 --- /dev/null +++ b/tokens/token-2022/default-account-state/steel/program/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "steel-program" +description = "" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +steel-api.workspace = true +solana-program.workspace = true +steel.workspace = true +spl-token-2022.workspace = true + +[dev-dependencies] +base64 = "0.21" +rand = "0.8.5" +solana-program-test = "2.1" +solana-sdk = "2.1" +tokio = { version = "1.35", features = ["full"] } +spl-associated-token-account = {version = "7.0.0", features = ["no-entrypoint"]} diff --git a/tokens/token-2022/default-account-state/steel/program/src/initialize.rs b/tokens/token-2022/default-account-state/steel/program/src/initialize.rs new file mode 100644 index 00000000..1e1da21f --- /dev/null +++ b/tokens/token-2022/default-account-state/steel/program/src/initialize.rs @@ -0,0 +1,55 @@ +use solana_program::{msg, program::invoke}; +use spl_token_2022::{ + extension::{ + default_account_state::instruction::initialize_default_account_state, ExtensionType, + }, + state::{AccountState, Mint}, +}; +use steel::*; + +pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { + // Load accounts. + let [signer_info, mint_info, system_program, token_program, rent_sysvar] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + msg!("Account loaded"); + + //Validation + signer_info.is_signer()?; + mint_info.is_empty()?.is_writable()?; + token_program.is_program(&spl_token_2022::ID)?; + system_program.is_program(&system_program::ID)?; + + //Calculate space for token and extension data + let space = + ExtensionType::try_calculate_account_len::(&[ExtensionType::DefaultAccountState])?; + + //Create new account for token and allocate space + create_account( + signer_info, + mint_info, + system_program, + space, + token_program.key, + )?; + + //Initialize the default account state extension + //This instruction must come after the instruction to initialize account + invoke( + &initialize_default_account_state(token_program.key, mint_info.key, &AccountState::Frozen)?, + &[mint_info.clone()], + )?; + + initialize_mint( + mint_info, + signer_info, + Some(signer_info), + token_program, + rent_sysvar, + 6, + )?; + + msg!("Default Account State Extension: Initialized."); + + Ok(()) +} diff --git a/tokens/token-2022/default-account-state/steel/program/src/lib.rs b/tokens/token-2022/default-account-state/steel/program/src/lib.rs new file mode 100644 index 00000000..8d9da29b --- /dev/null +++ b/tokens/token-2022/default-account-state/steel/program/src/lib.rs @@ -0,0 +1,27 @@ +mod initialize; +mod update_default_state; + +use initialize::*; +use update_default_state::*; + +use steel::*; +use steel_api::prelude::*; + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let (ix, data) = parse_instruction(&steel_api::ID, program_id, data)?; + + match ix { + SteelInstruction::Initialize => process_initialize(accounts, data)?, + SteelInstruction::UpdateDefaultAccountState => { + process_update_default_state(accounts, data)? + } + } + + Ok(()) +} + +entrypoint!(process_instruction); diff --git a/tokens/token-2022/default-account-state/steel/program/src/update_default_state.rs b/tokens/token-2022/default-account-state/steel/program/src/update_default_state.rs new file mode 100644 index 00000000..1f96f705 --- /dev/null +++ b/tokens/token-2022/default-account-state/steel/program/src/update_default_state.rs @@ -0,0 +1,34 @@ +use solana_program::{msg, program::invoke}; +use spl_token_2022::{ + extension::default_account_state::instruction::update_default_account_state, + state::AccountState, +}; +use steel::*; + +pub fn process_update_default_state(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { + // Load accounts. + let [signer_info, mint_info, system_program, token_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + //Validation + signer_info.is_signer()?; + token_program.is_program(&spl_token_2022::ID)?; + system_program.is_program(&system_program::ID)?; + + // Update Default Account State to Initialized + invoke( + &update_default_account_state( + token_program.key, + mint_info.key, + signer_info.key, + &[], + &AccountState::Initialized, + )?, + &[mint_info.clone(), signer_info.clone()], + )?; + + msg!("Default Account State Extension: State Updated."); + + Ok(()) +} diff --git a/tokens/token-2022/default-account-state/steel/program/tests/test.rs b/tokens/token-2022/default-account-state/steel/program/tests/test.rs new file mode 100644 index 00000000..f7020e6c --- /dev/null +++ b/tokens/token-2022/default-account-state/steel/program/tests/test.rs @@ -0,0 +1,128 @@ +use solana_program::hash::Hash; +use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_sdk::{ + program_pack::Pack, signature::Keypair, signer::Signer, system_instruction::create_account, + transaction::Transaction, +}; +use spl_associated_token_account::{ + get_associated_token_address_with_program_id, instruction::create_associated_token_account, +}; +use spl_token_2022::{ + instruction::{initialize_account3, mint_to}, + state::Account, +}; +use steel_api::prelude::*; + +async fn setup() -> (BanksClient, Keypair, Hash) { + let mut program_test = ProgramTest::new( + "steel_program", + steel_api::ID, + processor!(steel_program::process_instruction), + ); + program_test.prefer_bpf(true); + program_test.start().await +} + +#[tokio::test] +async fn run_test() { + // Setup test + let (mut banks, payer, blockhash) = setup().await; + let mint = Keypair::new(); + let receiver = Keypair::new(); + + // Submit initialize transaction. + let ix = initialize(payer.pubkey(), mint.pubkey()); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &mint], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + // //Create Ata and try out a transaction + let create_token_acc_ix1 = create_associated_token_account( + &payer.pubkey(), + &receiver.pubkey(), + &mint.pubkey(), + &spl_token_2022::ID, + ); + + let receiver_ata = get_associated_token_address_with_program_id( + &receiver.pubkey(), + &mint.pubkey(), + &spl_token_2022::ID, + ); + + //Mint Tokens + let mint_ix = mint_to( + &spl_token_2022::ID, + &mint.pubkey(), + &receiver_ata, + &payer.pubkey(), + &[], + 1_000_000_000, + ) + .unwrap(); + + let tx = Transaction::new_signed_with_payer( + &[create_token_acc_ix1, mint_ix], + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); + let res = banks.process_transaction(tx).await; + + assert!(res.is_err(), "MintTo unexpectedly succeeded"); + let err_string = format!("{:?}", res); + + assert!( + err_string.contains("Custom(17)"), + "Expected TokenError::AccountFrozen (17), got: {}", + err_string + ); + + //Update State and Try to Mint Again + let update_ix = update_default_account_state(payer.pubkey(), mint.pubkey()); + + let new_token_acc = Keypair::new(); + let create_ix = create_account( + &payer.pubkey(), + &new_token_acc.pubkey(), + banks + .get_rent() + .await + .unwrap() + .minimum_balance(Account::LEN), + Account::LEN as u64, + &spl_token_2022::ID, + ); + let init_ix = initialize_account3( + &spl_token_2022::ID, + &new_token_acc.pubkey(), + &mint.pubkey(), + &payer.pubkey(), + ) + .unwrap(); + + //Mint Tokens + let mint_ix2 = mint_to( + &spl_token_2022::ID, + &mint.pubkey(), + &new_token_acc.pubkey(), + &payer.pubkey(), + &[], + 1_000_000_000, + ) + .unwrap(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix, create_ix, init_ix, mint_ix2], + Some(&payer.pubkey()), + &[&payer, &mint, &new_token_acc], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); +} diff --git a/tokens/token-2022/immutable-owner/steel/.gitignore b/tokens/token-2022/immutable-owner/steel/.gitignore new file mode 100644 index 00000000..052739db --- /dev/null +++ b/tokens/token-2022/immutable-owner/steel/.gitignore @@ -0,0 +1,2 @@ +target +test-ledger diff --git a/tokens/token-2022/immutable-owner/steel/Cargo.toml b/tokens/token-2022/immutable-owner/steel/Cargo.toml new file mode 100644 index 00000000..fc9d5f88 --- /dev/null +++ b/tokens/token-2022/immutable-owner/steel/Cargo.toml @@ -0,0 +1,22 @@ +[workspace] +resolver = "2" +members = ["api", "program"] + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +homepage = "" +documentation = "" +repository = "" +readme = "./README.md" +keywords = ["solana"] + +[workspace.dependencies] +steel-api = { path = "./api", version = "0.1.0" } +bytemuck = "1.14" +num_enum = "0.7" +solana-program = "2.1" +steel = "3.0" +thiserror = "1.0" +spl-token-2022 = {version = "7.0.0", features = ["no-entrypoint"]} diff --git a/tokens/token-2022/immutable-owner/steel/README.md b/tokens/token-2022/immutable-owner/steel/README.md new file mode 100644 index 00000000..cfc508fd --- /dev/null +++ b/tokens/token-2022/immutable-owner/steel/README.md @@ -0,0 +1,28 @@ +# Steel + +**Steel** is a ... + +## API +- [`Consts`](api/src/consts.rs) – Program constants. +- [`Error`](api/src/error.rs) – Custom program errors. +- [`Event`](api/src/event.rs) – Custom program events. +- [`Instruction`](api/src/instruction.rs) – Declared instructions. + +## Instructions +- [`Add`](program/src/add.rs) – Add ... +- [`Initialize`](program/src/initialize.rs) – Initialize ... + +## State +- [`Counter`](api/src/state/counter.rs) – Counter ... + +## Get started + +Compile your program: +```sh +steel build +``` + +Run unit and integration tests: +```sh +steel test +``` diff --git a/tokens/token-2022/immutable-owner/steel/api/Cargo.toml b/tokens/token-2022/immutable-owner/steel/api/Cargo.toml new file mode 100644 index 00000000..e514258e --- /dev/null +++ b/tokens/token-2022/immutable-owner/steel/api/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "steel-api" +description = "API for interacting with the Steel program" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true + +[dependencies] +bytemuck.workspace = true +num_enum.workspace = true +solana-program.workspace = true +steel.workspace = true +thiserror.workspace = true +spl-token-2022.workspace = true diff --git a/tokens/token-2022/immutable-owner/steel/api/src/error.rs b/tokens/token-2022/immutable-owner/steel/api/src/error.rs new file mode 100644 index 00000000..96e84da4 --- /dev/null +++ b/tokens/token-2022/immutable-owner/steel/api/src/error.rs @@ -0,0 +1,10 @@ +use steel::*; + +#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, IntoPrimitive)] +#[repr(u32)] +pub enum SteelError { + #[error("This is a dummy error")] + Dummy = 0, +} + +error!(SteelError); diff --git a/tokens/token-2022/immutable-owner/steel/api/src/instruction.rs b/tokens/token-2022/immutable-owner/steel/api/src/instruction.rs new file mode 100644 index 00000000..c29bdee0 --- /dev/null +++ b/tokens/token-2022/immutable-owner/steel/api/src/instruction.rs @@ -0,0 +1,13 @@ +use steel::*; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] +pub enum SteelInstruction { + Initialize = 0, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct Initialize {} + +instruction!(SteelInstruction, Initialize); diff --git a/tokens/token-2022/immutable-owner/steel/api/src/lib.rs b/tokens/token-2022/immutable-owner/steel/api/src/lib.rs new file mode 100644 index 00000000..2c146dac --- /dev/null +++ b/tokens/token-2022/immutable-owner/steel/api/src/lib.rs @@ -0,0 +1,14 @@ +pub mod error; +pub mod instruction; +pub mod sdk; + +pub mod prelude { + pub use crate::error::*; + pub use crate::instruction::*; + pub use crate::sdk::*; +} + +use steel::*; + +// TODO Set program id +declare_id!("z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35"); diff --git a/tokens/token-2022/immutable-owner/steel/api/src/sdk.rs b/tokens/token-2022/immutable-owner/steel/api/src/sdk.rs new file mode 100644 index 00000000..c142f57e --- /dev/null +++ b/tokens/token-2022/immutable-owner/steel/api/src/sdk.rs @@ -0,0 +1,17 @@ +use steel::*; + +use crate::prelude::*; + +pub fn initialize(signer: Pubkey, mint: Pubkey, token_account: Pubkey) -> Instruction { + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(signer, true), + AccountMeta::new(mint, false), + AccountMeta::new(token_account, true), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(spl_token_2022::ID, false), + ], + data: Initialize {}.to_bytes(), + } +} diff --git a/tokens/token-2022/immutable-owner/steel/program/Cargo.toml b/tokens/token-2022/immutable-owner/steel/program/Cargo.toml new file mode 100644 index 00000000..11f2289d --- /dev/null +++ b/tokens/token-2022/immutable-owner/steel/program/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "steel-program" +description = "" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +steel-api.workspace = true +solana-program.workspace = true +steel.workspace = true +spl-token-2022.workspace = true + +[dev-dependencies] +base64 = "0.21" +rand = "0.8.5" +solana-program-test = "2.1" +solana-sdk = "2.1" +tokio = { version = "1.35", features = ["full"] } diff --git a/tokens/token-2022/immutable-owner/steel/program/src/initialize.rs b/tokens/token-2022/immutable-owner/steel/program/src/initialize.rs new file mode 100644 index 00000000..e7e08d96 --- /dev/null +++ b/tokens/token-2022/immutable-owner/steel/program/src/initialize.rs @@ -0,0 +1,58 @@ +use solana_program::{msg, program::invoke}; +use spl_token_2022::{ + extension::ExtensionType, + instruction::{initialize_account3, initialize_immutable_owner}, + state::Account, +}; +use steel::*; + +pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { + // Load accounts. + let [signer_info, mint_info, token_account_info, system_program, token_program] = accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + msg!("Account loaded"); + + //Validation + signer_info.is_signer()?; + token_account_info.is_writable()?; + token_program.is_program(&spl_token_2022::ID)?; + system_program.is_program(&system_program::ID)?; + + //Calculate space for token and extension data + let space = + ExtensionType::try_calculate_account_len::(&[ExtensionType::ImmutableOwner])?; + + //Create new account for token and allocate space + create_account( + signer_info, + token_account_info, + system_program, + space, + token_program.key, + )?; + + msg!(&token_account_info.key.to_string()); + + //Initialize the immutable owner extension + //This instruction must come before the instruction to initialize account + invoke( + &initialize_immutable_owner(token_program.key, token_account_info.key)?, + &[token_account_info.clone()], + )?; + + invoke( + &initialize_account3( + token_program.key, + token_account_info.key, + mint_info.key, + signer_info.key, + )?, + &[token_account_info.clone(), mint_info.clone()], + )?; + + msg!("Immutable Owner Extension: Initialized."); + + Ok(()) +} diff --git a/tokens/token-2022/immutable-owner/steel/program/src/lib.rs b/tokens/token-2022/immutable-owner/steel/program/src/lib.rs new file mode 100644 index 00000000..73785439 --- /dev/null +++ b/tokens/token-2022/immutable-owner/steel/program/src/lib.rs @@ -0,0 +1,22 @@ +mod initialize; + +use initialize::*; + +use steel::*; +use steel_api::prelude::*; + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let (ix, data) = parse_instruction(&steel_api::ID, program_id, data)?; + + match ix { + SteelInstruction::Initialize => process_initialize(accounts, data)?, + } + + Ok(()) +} + +entrypoint!(process_instruction); diff --git a/tokens/token-2022/immutable-owner/steel/program/tests/test.rs b/tokens/token-2022/immutable-owner/steel/program/tests/test.rs new file mode 100644 index 00000000..228c9895 --- /dev/null +++ b/tokens/token-2022/immutable-owner/steel/program/tests/test.rs @@ -0,0 +1,86 @@ +use solana_program::hash::Hash; +use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_sdk::{ + program_pack::Pack, signature::Keypair, signer::Signer, system_instruction::create_account, + transaction::Transaction, +}; +use spl_token_2022::{ + instruction::{initialize_mint2, mint_to, set_authority, AuthorityType}, + state::Mint, +}; +use steel_api::prelude::*; + +async fn setup() -> (BanksClient, Keypair, Hash) { + let mut program_test = ProgramTest::new( + "steel_program", + steel_api::ID, + processor!(steel_program::process_instruction), + ); + program_test.prefer_bpf(true); + program_test.start().await +} + +#[tokio::test] +async fn run_test() { + // Setup test + let (mut banks, payer, blockhash) = setup().await; + + let mint = Keypair::new(); + let token_acc = Keypair::new(); + let receiver_token_acc = Keypair::new(); + + let create_acc_ix = create_account( + &payer.pubkey(), + &mint.pubkey(), + banks.get_rent().await.unwrap().minimum_balance(Mint::LEN), + Mint::LEN as u64, + &spl_token_2022::ID, + ); + let init_mint_ix = initialize_mint2( + &spl_token_2022::ID, + &mint.pubkey(), + &payer.pubkey(), + None, + 6, + ) + .unwrap(); + let mint_to_ix = mint_to( + &spl_token_2022::ID, + &mint.pubkey(), + &token_acc.pubkey(), + &payer.pubkey(), + &[], + 1_000_000, + ) + .unwrap(); + + //Try to set authority + let auth_ix = set_authority( + &spl_token_2022::ID, + &token_acc.pubkey(), + Some(&receiver_token_acc.pubkey()), + AuthorityType::AccountOwner, + &payer.pubkey(), + &[], + ) + .unwrap(); + + // Submit initialize transaction. + let ix = initialize(payer.pubkey(), mint.pubkey(), token_acc.pubkey()); + let ix2 = initialize(payer.pubkey(), mint.pubkey(), receiver_token_acc.pubkey()); + let tx = Transaction::new_signed_with_payer( + &[create_acc_ix, init_mint_ix, ix, ix2, mint_to_ix, auth_ix], + Some(&payer.pubkey()), + &[&payer, &mint, &token_acc, &receiver_token_acc], + blockhash, + ); + let res = banks.process_transaction(tx).await; + + let err_string = format!("{:?}", res); + + assert!( + err_string.contains("Custom(34)"), + "Expected TokenError::OwnerCannotBeChanged (34), got: {}", + err_string + ); +} diff --git a/tokens/token-2022/interest-bearing/steel/.gitignore b/tokens/token-2022/interest-bearing/steel/.gitignore new file mode 100644 index 00000000..052739db --- /dev/null +++ b/tokens/token-2022/interest-bearing/steel/.gitignore @@ -0,0 +1,2 @@ +target +test-ledger diff --git a/tokens/token-2022/interest-bearing/steel/Cargo.toml b/tokens/token-2022/interest-bearing/steel/Cargo.toml new file mode 100644 index 00000000..23402b9a --- /dev/null +++ b/tokens/token-2022/interest-bearing/steel/Cargo.toml @@ -0,0 +1,22 @@ +[workspace] +resolver = "2" +members = ["api", "program"] + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +homepage = "" +documentation = "" +repository = "" +readme = "./README.md" +keywords = ["solana"] + +[workspace.dependencies] +steel-api = { path = "./api", version = "0.1.0" } +bytemuck = "1.14" +num_enum = "0.7" +solana-program = "2.1" +steel = "4.0" +thiserror = "1.0" +spl-token-2022 = {version = "7.0.0", features = ["no-entrypoint"]} diff --git a/tokens/token-2022/interest-bearing/steel/README.md b/tokens/token-2022/interest-bearing/steel/README.md new file mode 100644 index 00000000..cfc508fd --- /dev/null +++ b/tokens/token-2022/interest-bearing/steel/README.md @@ -0,0 +1,28 @@ +# Steel + +**Steel** is a ... + +## API +- [`Consts`](api/src/consts.rs) – Program constants. +- [`Error`](api/src/error.rs) – Custom program errors. +- [`Event`](api/src/event.rs) – Custom program events. +- [`Instruction`](api/src/instruction.rs) – Declared instructions. + +## Instructions +- [`Add`](program/src/add.rs) – Add ... +- [`Initialize`](program/src/initialize.rs) – Initialize ... + +## State +- [`Counter`](api/src/state/counter.rs) – Counter ... + +## Get started + +Compile your program: +```sh +steel build +``` + +Run unit and integration tests: +```sh +steel test +``` diff --git a/tokens/token-2022/interest-bearing/steel/api/Cargo.toml b/tokens/token-2022/interest-bearing/steel/api/Cargo.toml new file mode 100644 index 00000000..e514258e --- /dev/null +++ b/tokens/token-2022/interest-bearing/steel/api/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "steel-api" +description = "API for interacting with the Steel program" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true + +[dependencies] +bytemuck.workspace = true +num_enum.workspace = true +solana-program.workspace = true +steel.workspace = true +thiserror.workspace = true +spl-token-2022.workspace = true diff --git a/tokens/token-2022/interest-bearing/steel/api/src/error.rs b/tokens/token-2022/interest-bearing/steel/api/src/error.rs new file mode 100644 index 00000000..96e84da4 --- /dev/null +++ b/tokens/token-2022/interest-bearing/steel/api/src/error.rs @@ -0,0 +1,10 @@ +use steel::*; + +#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, IntoPrimitive)] +#[repr(u32)] +pub enum SteelError { + #[error("This is a dummy error")] + Dummy = 0, +} + +error!(SteelError); diff --git a/tokens/token-2022/interest-bearing/steel/api/src/instruction.rs b/tokens/token-2022/interest-bearing/steel/api/src/instruction.rs new file mode 100644 index 00000000..658ce73c --- /dev/null +++ b/tokens/token-2022/interest-bearing/steel/api/src/instruction.rs @@ -0,0 +1,22 @@ +use steel::*; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] +pub enum SteelInstruction { + Initialize = 0, + UpdateRate = 1, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct Initialize { + pub rate: [u8; 2], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct UpdateRate { + pub rate: [u8; 2], +} +instruction!(SteelInstruction, Initialize); +instruction!(SteelInstruction, UpdateRate); diff --git a/tokens/token-2022/interest-bearing/steel/api/src/lib.rs b/tokens/token-2022/interest-bearing/steel/api/src/lib.rs new file mode 100644 index 00000000..2c146dac --- /dev/null +++ b/tokens/token-2022/interest-bearing/steel/api/src/lib.rs @@ -0,0 +1,14 @@ +pub mod error; +pub mod instruction; +pub mod sdk; + +pub mod prelude { + pub use crate::error::*; + pub use crate::instruction::*; + pub use crate::sdk::*; +} + +use steel::*; + +// TODO Set program id +declare_id!("z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35"); diff --git a/tokens/token-2022/interest-bearing/steel/api/src/sdk.rs b/tokens/token-2022/interest-bearing/steel/api/src/sdk.rs new file mode 100644 index 00000000..1a2ce727 --- /dev/null +++ b/tokens/token-2022/interest-bearing/steel/api/src/sdk.rs @@ -0,0 +1,37 @@ +use steel::*; + +use crate::prelude::*; + +pub fn initialize(signer: Pubkey, mint: Pubkey, rate: i16) -> Instruction { + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(signer, true), + AccountMeta::new(mint, true), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(spl_token_2022::ID, false), + AccountMeta::new_readonly(sysvar::rent::ID, false), + ], + data: Initialize { + rate: rate.to_le_bytes(), + } + .to_bytes(), + } +} + +pub fn update_rate(signer: Pubkey, mint: Pubkey, rate: i16) -> Instruction { + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(signer, true), + AccountMeta::new(mint, false), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(spl_token_2022::ID, false), + ], + data: UpdateRate { + rate: rate.to_le_bytes(), + } + .to_bytes(), + } +} + diff --git a/tokens/token-2022/interest-bearing/steel/program/Cargo.toml b/tokens/token-2022/interest-bearing/steel/program/Cargo.toml new file mode 100644 index 00000000..11f2289d --- /dev/null +++ b/tokens/token-2022/interest-bearing/steel/program/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "steel-program" +description = "" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +steel-api.workspace = true +solana-program.workspace = true +steel.workspace = true +spl-token-2022.workspace = true + +[dev-dependencies] +base64 = "0.21" +rand = "0.8.5" +solana-program-test = "2.1" +solana-sdk = "2.1" +tokio = { version = "1.35", features = ["full"] } diff --git a/tokens/token-2022/interest-bearing/steel/program/src/initialize.rs b/tokens/token-2022/interest-bearing/steel/program/src/initialize.rs new file mode 100644 index 00000000..170c8495 --- /dev/null +++ b/tokens/token-2022/interest-bearing/steel/program/src/initialize.rs @@ -0,0 +1,60 @@ +use solana_program::{msg, program::invoke}; +use spl_token_2022::{ + extension::{interest_bearing_mint, ExtensionType}, + state::Mint, +}; +use steel::*; +use steel_api::prelude::*; + +pub fn process_initialize(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { + let args = Initialize::try_from_bytes(&data)?; + let rate = i16::from_le_bytes(args.rate); + + // Load accounts. + let [signer_info, mint_info, system_program, token_program, rent_sysvar] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + signer_info.is_signer()?; + mint_info.is_signer()?.is_empty()?.is_writable()?; + system_program.is_program(&system_program::ID)?; + token_program.is_program(&spl_token_2022::ID)?; + rent_sysvar.is_sysvar(&sysvar::rent::ID)?; + + //Calculate space for mint and extension data + let space = + ExtensionType::try_calculate_account_len::(&[ExtensionType::InterestBearingConfig])?; + //Create account for the mint and allocate space + create_account( + signer_info, + mint_info, + system_program, + space, + token_program.key, + )?; + + //Initialize the Interest Bearing Mint Extension + //This instruction must come before the instruction to initialize mint data + invoke( + &interest_bearing_mint::instruction::initialize( + token_program.key, + mint_info.key, + Some(*signer_info.key), + rate, + )?, + &[mint_info.clone(), signer_info.clone()], + )?; + + initialize_mint( + mint_info, + signer_info, + Some(signer_info), + token_program, + rent_sysvar, + 6, //you can configure it to be passed as instruction data + )?; + + msg!("Interest Bearing Mint Extension: Initialized!"); + + Ok(()) +} diff --git a/tokens/token-2022/interest-bearing/steel/program/src/lib.rs b/tokens/token-2022/interest-bearing/steel/program/src/lib.rs new file mode 100644 index 00000000..6af4d4a2 --- /dev/null +++ b/tokens/token-2022/interest-bearing/steel/program/src/lib.rs @@ -0,0 +1,25 @@ +mod initialize; +mod update_fee; + +use initialize::*; +use update_fee::*; + +use steel::*; +use steel_api::prelude::*; + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let (ix, data) = parse_instruction(&steel_api::ID, program_id, data)?; + + match ix { + SteelInstruction::Initialize => process_initialize(accounts, data)?, + SteelInstruction::UpdateRate => process_update_rate(accounts, data)?, + } + + Ok(()) +} + +entrypoint!(process_instruction); diff --git a/tokens/token-2022/interest-bearing/steel/program/src/update_fee.rs b/tokens/token-2022/interest-bearing/steel/program/src/update_fee.rs new file mode 100644 index 00000000..da55eac0 --- /dev/null +++ b/tokens/token-2022/interest-bearing/steel/program/src/update_fee.rs @@ -0,0 +1,33 @@ +use solana_program::{msg, program::invoke}; +use spl_token_2022::extension::interest_bearing_mint; +use steel::*; +use steel_api::prelude::*; + +pub fn process_update_rate(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { + let args = UpdateRate::try_from_bytes(&data)?; + let rate = i16::from_le_bytes(args.rate); + + // Load accounts. + let [signer_info, mint_info, system_program, token_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + signer_info.is_signer()?; + system_program.is_program(&system_program::ID)?; + token_program.is_program(&spl_token_2022::ID)?; + + invoke( + &interest_bearing_mint::instruction::update_rate( + token_program.key, + mint_info.key, + &signer_info.key, + &[], + rate, + )?, + &[mint_info.clone(), signer_info.clone()], + )?; + + msg!("Interest Bearing Mint Extension: Rate Updated!"); + + Ok(()) +} diff --git a/tokens/token-2022/interest-bearing/steel/program/tests/test.rs b/tokens/token-2022/interest-bearing/steel/program/tests/test.rs new file mode 100644 index 00000000..4fd9218e --- /dev/null +++ b/tokens/token-2022/interest-bearing/steel/program/tests/test.rs @@ -0,0 +1,77 @@ +use solana_program::hash::Hash; +use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_sdk::{signature::Keypair, signer::Signer, transaction::Transaction}; +use spl_token_2022::{extension::{interest_bearing_mint::InterestBearingConfig, BaseStateWithExtensions, StateWithExtensions}, state::Mint}; +use steel::*; +use steel_api::prelude::*; + +async fn setup() -> (BanksClient, Keypair, Hash) { + let mut program_test = ProgramTest::new( + "steel_program", + steel_api::ID, + processor!(steel_program::process_instruction), + ); + program_test.prefer_bpf(true); + program_test.start().await +} + +#[tokio::test] +async fn run_test() { + // Setup test + let (mut banks, payer, blockhash) = setup().await; + let mint = Keypair::new(); + + // Submit initialize transaction. + let ix = initialize(payer.pubkey(), mint.pubkey(), 100); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &mint], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + //Get mint data + let mint_account = banks.get_account(mint.pubkey()).await.unwrap().unwrap(); + + let mint_data = mint_account.data.as_slice(); + let state_with_extensions = StateWithExtensions::::unpack(mint_data).unwrap(); + let extension = state_with_extensions + .get_extension::() + .unwrap(); + + assert_eq!( + i16::from_le_bytes(extension.current_rate.0), + 100, + "Interest Rate Mismatch" + ); + + // Update Rate + let ix: Instruction = update_rate(payer.pubkey(), mint.pubkey(), 1500); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + + //Get mint data + let mint_account = banks.get_account(mint.pubkey()).await.unwrap().unwrap(); + + let mint_data = mint_account.data.as_slice(); + let state_with_extensions = StateWithExtensions::::unpack(mint_data).unwrap(); + let extension = state_with_extensions + .get_extension::() + .unwrap(); + + assert_eq!( + i16::from_le_bytes(extension.current_rate.0), + 1500, + "Interest Rate Mismatch" + ); + +} diff --git a/tokens/token-2022/memo-transfer/steel/Cargo.toml b/tokens/token-2022/memo-transfer/steel/Cargo.toml new file mode 100644 index 00000000..23402b9a --- /dev/null +++ b/tokens/token-2022/memo-transfer/steel/Cargo.toml @@ -0,0 +1,22 @@ +[workspace] +resolver = "2" +members = ["api", "program"] + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +homepage = "" +documentation = "" +repository = "" +readme = "./README.md" +keywords = ["solana"] + +[workspace.dependencies] +steel-api = { path = "./api", version = "0.1.0" } +bytemuck = "1.14" +num_enum = "0.7" +solana-program = "2.1" +steel = "4.0" +thiserror = "1.0" +spl-token-2022 = {version = "7.0.0", features = ["no-entrypoint"]} diff --git a/tokens/token-2022/memo-transfer/steel/README.md b/tokens/token-2022/memo-transfer/steel/README.md new file mode 100644 index 00000000..cfc508fd --- /dev/null +++ b/tokens/token-2022/memo-transfer/steel/README.md @@ -0,0 +1,28 @@ +# Steel + +**Steel** is a ... + +## API +- [`Consts`](api/src/consts.rs) – Program constants. +- [`Error`](api/src/error.rs) – Custom program errors. +- [`Event`](api/src/event.rs) – Custom program events. +- [`Instruction`](api/src/instruction.rs) – Declared instructions. + +## Instructions +- [`Add`](program/src/add.rs) – Add ... +- [`Initialize`](program/src/initialize.rs) – Initialize ... + +## State +- [`Counter`](api/src/state/counter.rs) – Counter ... + +## Get started + +Compile your program: +```sh +steel build +``` + +Run unit and integration tests: +```sh +steel test +``` diff --git a/tokens/token-2022/memo-transfer/steel/api/Cargo.toml b/tokens/token-2022/memo-transfer/steel/api/Cargo.toml new file mode 100644 index 00000000..e514258e --- /dev/null +++ b/tokens/token-2022/memo-transfer/steel/api/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "steel-api" +description = "API for interacting with the Steel program" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true + +[dependencies] +bytemuck.workspace = true +num_enum.workspace = true +solana-program.workspace = true +steel.workspace = true +thiserror.workspace = true +spl-token-2022.workspace = true diff --git a/tokens/token-2022/memo-transfer/steel/api/src/error.rs b/tokens/token-2022/memo-transfer/steel/api/src/error.rs new file mode 100644 index 00000000..96e84da4 --- /dev/null +++ b/tokens/token-2022/memo-transfer/steel/api/src/error.rs @@ -0,0 +1,10 @@ +use steel::*; + +#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, IntoPrimitive)] +#[repr(u32)] +pub enum SteelError { + #[error("This is a dummy error")] + Dummy = 0, +} + +error!(SteelError); diff --git a/tokens/token-2022/memo-transfer/steel/api/src/instruction.rs b/tokens/token-2022/memo-transfer/steel/api/src/instruction.rs new file mode 100644 index 00000000..c29bdee0 --- /dev/null +++ b/tokens/token-2022/memo-transfer/steel/api/src/instruction.rs @@ -0,0 +1,13 @@ +use steel::*; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] +pub enum SteelInstruction { + Initialize = 0, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct Initialize {} + +instruction!(SteelInstruction, Initialize); diff --git a/tokens/token-2022/memo-transfer/steel/api/src/lib.rs b/tokens/token-2022/memo-transfer/steel/api/src/lib.rs new file mode 100644 index 00000000..2c146dac --- /dev/null +++ b/tokens/token-2022/memo-transfer/steel/api/src/lib.rs @@ -0,0 +1,14 @@ +pub mod error; +pub mod instruction; +pub mod sdk; + +pub mod prelude { + pub use crate::error::*; + pub use crate::instruction::*; + pub use crate::sdk::*; +} + +use steel::*; + +// TODO Set program id +declare_id!("z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35"); diff --git a/tokens/token-2022/memo-transfer/steel/api/src/sdk.rs b/tokens/token-2022/memo-transfer/steel/api/src/sdk.rs new file mode 100644 index 00000000..c142f57e --- /dev/null +++ b/tokens/token-2022/memo-transfer/steel/api/src/sdk.rs @@ -0,0 +1,17 @@ +use steel::*; + +use crate::prelude::*; + +pub fn initialize(signer: Pubkey, mint: Pubkey, token_account: Pubkey) -> Instruction { + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(signer, true), + AccountMeta::new(mint, false), + AccountMeta::new(token_account, true), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(spl_token_2022::ID, false), + ], + data: Initialize {}.to_bytes(), + } +} diff --git a/tokens/token-2022/memo-transfer/steel/program/Cargo.toml b/tokens/token-2022/memo-transfer/steel/program/Cargo.toml new file mode 100644 index 00000000..9692fde6 --- /dev/null +++ b/tokens/token-2022/memo-transfer/steel/program/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "steel-program" +description = "" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +steel-api.workspace = true +solana-program.workspace = true +steel.workspace = true +spl-token-2022.workspace = true + +[dev-dependencies] +base64 = "0.21" +rand = "0.8.5" +solana-program-test = "2.1" +solana-sdk = "2.1" +tokio = { version = "1.35", features = ["full"] } +spl-memo = {version = "6.0.0", features = ["no-entrypoint"]} diff --git a/tokens/token-2022/memo-transfer/steel/program/src/initialize.rs b/tokens/token-2022/memo-transfer/steel/program/src/initialize.rs new file mode 100644 index 00000000..7161e36a --- /dev/null +++ b/tokens/token-2022/memo-transfer/steel/program/src/initialize.rs @@ -0,0 +1,63 @@ +use solana_program::{msg, program::invoke}; +use spl_token_2022::{ + extension::{memo_transfer::instruction::enable_required_transfer_memos, ExtensionType}, + instruction::initialize_account3, + state::Account, +}; +use steel::*; + +pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { + // Load accounts. + let [signer_info, mint_info, token_account_info, system_program, token_program] = accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + msg!("Account loaded"); + + //Validation + signer_info.is_signer()?; + token_account_info.is_writable()?; + token_program.is_program(&spl_token_2022::ID)?; + system_program.is_program(&system_program::ID)?; + + //Calculate space for token and extension data + let space = + ExtensionType::try_calculate_account_len::(&[ExtensionType::MemoTransfer])?; + + //Create new account for token and allocate space + create_account( + signer_info, + token_account_info, + system_program, + space, + token_program.key, + )?; + + msg!(&token_account_info.key.to_string()); + + invoke( + &initialize_account3( + token_program.key, + token_account_info.key, + mint_info.key, + signer_info.key, + )?, + &[token_account_info.clone(), mint_info.clone()], + )?; + + //Initialize the memo on transfer extension + //This instruction must come after the instruction to initialize account + invoke( + &enable_required_transfer_memos( + token_program.key, + token_account_info.key, + signer_info.key, + &[], + )?, + &[token_account_info.clone(), signer_info.clone()], + )?; + + msg!("Memo Transfer Extension: Initialized."); + + Ok(()) +} diff --git a/tokens/token-2022/memo-transfer/steel/program/src/lib.rs b/tokens/token-2022/memo-transfer/steel/program/src/lib.rs new file mode 100644 index 00000000..73785439 --- /dev/null +++ b/tokens/token-2022/memo-transfer/steel/program/src/lib.rs @@ -0,0 +1,22 @@ +mod initialize; + +use initialize::*; + +use steel::*; +use steel_api::prelude::*; + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let (ix, data) = parse_instruction(&steel_api::ID, program_id, data)?; + + match ix { + SteelInstruction::Initialize => process_initialize(accounts, data)?, + } + + Ok(()) +} + +entrypoint!(process_instruction); diff --git a/tokens/token-2022/memo-transfer/steel/program/tests/test.rs b/tokens/token-2022/memo-transfer/steel/program/tests/test.rs new file mode 100644 index 00000000..f2e6f96b --- /dev/null +++ b/tokens/token-2022/memo-transfer/steel/program/tests/test.rs @@ -0,0 +1,120 @@ +use solana_program::hash::Hash; +use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_sdk::{ + program_pack::Pack, signature::Keypair, signer::Signer, system_instruction::create_account, transaction::Transaction +}; + +use spl_memo::build_memo; +use spl_token_2022::{ + instruction::{initialize_mint2, mint_to, transfer_checked}, + state::Mint, +}; +use steel_api::prelude::*; + +async fn setup() -> (BanksClient, Keypair, Hash) { + let mut program_test = ProgramTest::new( + "steel_program", + steel_api::ID, + processor!(steel_program::process_instruction), + ); + program_test.prefer_bpf(true); + program_test.start().await +} + +#[tokio::test] +async fn run_test() { + // Setup test + let (mut banks, payer, blockhash) = setup().await; + + let mint = Keypair::new(); + let token_acc = Keypair::new(); + let receiver_token_acc = Keypair::new(); + + let create_acc_ix = create_account( + &payer.pubkey(), + &mint.pubkey(), + banks.get_rent().await.unwrap().minimum_balance(Mint::LEN), + Mint::LEN as u64, + &spl_token_2022::ID, + ); + let init_mint_ix = initialize_mint2( + &spl_token_2022::ID, + &mint.pubkey(), + &payer.pubkey(), + None, + 6, + ) + .unwrap(); + let mint_to_ix = mint_to( + &spl_token_2022::ID, + &mint.pubkey(), + &token_acc.pubkey(), + &payer.pubkey(), + &[], + 1_000_000, + ) + .unwrap(); + + // Submit initialize transaction. + let ix = initialize(payer.pubkey(), mint.pubkey(), token_acc.pubkey()); + let ix2 = initialize(payer.pubkey(), mint.pubkey(), receiver_token_acc.pubkey()); + let tx = Transaction::new_signed_with_payer( + &[create_acc_ix, init_mint_ix, ix, ix2, mint_to_ix], + Some(&payer.pubkey()), + &[&payer, &mint, &token_acc, &receiver_token_acc], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + // Try transfer without memo + let transfer_ix = transfer_checked( + &spl_token_2022::ID, + &token_acc.pubkey(), + &mint.pubkey(), + &receiver_token_acc.pubkey(), + &payer.pubkey(), + &[], + 100, + 6, + ) + .unwrap(); + let tx = Transaction::new_signed_with_payer( + &[transfer_ix], + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); + let res = banks.process_transaction(tx).await; + let err_string = format!("{:?}", res); + + // Custom(0x24) = Custom(36) = TokenError::MissingMemo + assert!( + err_string.contains("Custom(36)"), + "Expected TokenError::MissingMemo (36), got: {}", + err_string + ); + + //Try Transfer with memo + let memo = "Memo from Steel Token2022 Examples"; + let memo_ix = build_memo(memo.as_bytes(), &[&payer.pubkey()]); + let transfer_ix = transfer_checked( + &spl_token_2022::ID, + &token_acc.pubkey(), + &mint.pubkey(), + &receiver_token_acc.pubkey(), + &payer.pubkey(), + &[], + 100, + 6, + ) + .unwrap(); + let tx = Transaction::new_signed_with_payer( + &[memo_ix, transfer_ix], + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); +} diff --git a/tokens/token-2022/mint-close-authority/steel/.gitignore b/tokens/token-2022/mint-close-authority/steel/.gitignore new file mode 100644 index 00000000..052739db --- /dev/null +++ b/tokens/token-2022/mint-close-authority/steel/.gitignore @@ -0,0 +1,2 @@ +target +test-ledger diff --git a/tokens/token-2022/mint-close-authority/steel/Cargo.toml b/tokens/token-2022/mint-close-authority/steel/Cargo.toml new file mode 100644 index 00000000..23402b9a --- /dev/null +++ b/tokens/token-2022/mint-close-authority/steel/Cargo.toml @@ -0,0 +1,22 @@ +[workspace] +resolver = "2" +members = ["api", "program"] + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +homepage = "" +documentation = "" +repository = "" +readme = "./README.md" +keywords = ["solana"] + +[workspace.dependencies] +steel-api = { path = "./api", version = "0.1.0" } +bytemuck = "1.14" +num_enum = "0.7" +solana-program = "2.1" +steel = "4.0" +thiserror = "1.0" +spl-token-2022 = {version = "7.0.0", features = ["no-entrypoint"]} diff --git a/tokens/token-2022/mint-close-authority/steel/README.md b/tokens/token-2022/mint-close-authority/steel/README.md new file mode 100644 index 00000000..cfc508fd --- /dev/null +++ b/tokens/token-2022/mint-close-authority/steel/README.md @@ -0,0 +1,28 @@ +# Steel + +**Steel** is a ... + +## API +- [`Consts`](api/src/consts.rs) – Program constants. +- [`Error`](api/src/error.rs) – Custom program errors. +- [`Event`](api/src/event.rs) – Custom program events. +- [`Instruction`](api/src/instruction.rs) – Declared instructions. + +## Instructions +- [`Add`](program/src/add.rs) – Add ... +- [`Initialize`](program/src/initialize.rs) – Initialize ... + +## State +- [`Counter`](api/src/state/counter.rs) – Counter ... + +## Get started + +Compile your program: +```sh +steel build +``` + +Run unit and integration tests: +```sh +steel test +``` diff --git a/tokens/token-2022/mint-close-authority/steel/api/Cargo.toml b/tokens/token-2022/mint-close-authority/steel/api/Cargo.toml new file mode 100644 index 00000000..e514258e --- /dev/null +++ b/tokens/token-2022/mint-close-authority/steel/api/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "steel-api" +description = "API for interacting with the Steel program" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true + +[dependencies] +bytemuck.workspace = true +num_enum.workspace = true +solana-program.workspace = true +steel.workspace = true +thiserror.workspace = true +spl-token-2022.workspace = true diff --git a/tokens/token-2022/mint-close-authority/steel/api/src/error.rs b/tokens/token-2022/mint-close-authority/steel/api/src/error.rs new file mode 100644 index 00000000..96e84da4 --- /dev/null +++ b/tokens/token-2022/mint-close-authority/steel/api/src/error.rs @@ -0,0 +1,10 @@ +use steel::*; + +#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, IntoPrimitive)] +#[repr(u32)] +pub enum SteelError { + #[error("This is a dummy error")] + Dummy = 0, +} + +error!(SteelError); diff --git a/tokens/token-2022/mint-close-authority/steel/api/src/instruction.rs b/tokens/token-2022/mint-close-authority/steel/api/src/instruction.rs new file mode 100644 index 00000000..041284b9 --- /dev/null +++ b/tokens/token-2022/mint-close-authority/steel/api/src/instruction.rs @@ -0,0 +1,19 @@ +use steel::*; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] +pub enum SteelInstruction { + Initialize = 0, + MintCloseAuthority = 1, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct Initialize {} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct MintCloseAuthority {} + +instruction!(SteelInstruction, Initialize); +instruction!(SteelInstruction, MintCloseAuthority); diff --git a/tokens/token-2022/mint-close-authority/steel/api/src/lib.rs b/tokens/token-2022/mint-close-authority/steel/api/src/lib.rs new file mode 100644 index 00000000..2c146dac --- /dev/null +++ b/tokens/token-2022/mint-close-authority/steel/api/src/lib.rs @@ -0,0 +1,14 @@ +pub mod error; +pub mod instruction; +pub mod sdk; + +pub mod prelude { + pub use crate::error::*; + pub use crate::instruction::*; + pub use crate::sdk::*; +} + +use steel::*; + +// TODO Set program id +declare_id!("z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35"); diff --git a/tokens/token-2022/mint-close-authority/steel/api/src/sdk.rs b/tokens/token-2022/mint-close-authority/steel/api/src/sdk.rs new file mode 100644 index 00000000..c1f20c8b --- /dev/null +++ b/tokens/token-2022/mint-close-authority/steel/api/src/sdk.rs @@ -0,0 +1,31 @@ +use steel::*; + +use crate::prelude::*; + +pub fn initialize(signer: Pubkey, mint: Pubkey) -> Instruction { + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(signer, true), + AccountMeta::new(mint, true), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(spl_token_2022::ID, false), + AccountMeta::new_readonly(sysvar::rent::ID, false), + ], + data: Initialize {}.to_bytes(), + } +} + +pub fn mint_close(signer: Pubkey, destination: Pubkey, mint: Pubkey) -> Instruction { + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(signer, true), + AccountMeta::new(mint, true), + AccountMeta::new(destination, false), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(spl_token_2022::ID, false), + ], + data: MintCloseAuthority {}.to_bytes(), + } +} diff --git a/tokens/token-2022/mint-close-authority/steel/program/Cargo.toml b/tokens/token-2022/mint-close-authority/steel/program/Cargo.toml new file mode 100644 index 00000000..2389cae0 --- /dev/null +++ b/tokens/token-2022/mint-close-authority/steel/program/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "steel-program" +description = "" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +steel-api.workspace = true +solana-program.workspace = true +steel.workspace = true +spl-token-2022.workspace = true + +[dev-dependencies] +base64 = "0.21" +rand = "0.8.5" +solana-program-test = "2.1" +solana-sdk = "2.1" +tokio = { version = "1.35", features = ["full"] } +solana-keypair = "2.2" +solana-signer = "2.2" +solana-transaction = "2.2" diff --git a/tokens/token-2022/mint-close-authority/steel/program/src/initialize.rs b/tokens/token-2022/mint-close-authority/steel/program/src/initialize.rs new file mode 100644 index 00000000..310563cd --- /dev/null +++ b/tokens/token-2022/mint-close-authority/steel/program/src/initialize.rs @@ -0,0 +1,54 @@ +use solana_program::{msg, program::invoke}; +use spl_token_2022::{ + extension::ExtensionType, + instruction::initialize_mint_close_authority, + state::Mint, +}; +use steel::*; + +//Here the close authority is the signer info +pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { + // Load accounts. + let [signer_info, mint_info, system_program, token_program, rent_sysvar] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + signer_info.is_signer()?; + mint_info.is_signer()?.is_empty()?.is_writable()?; + system_program.is_program(&system_program::ID)?; + token_program.is_program(&spl_token_2022::ID)?; + rent_sysvar.is_sysvar(&sysvar::rent::ID)?; + + //Calculate space for mint and extension data + let space = + ExtensionType::try_calculate_account_len::(&[ExtensionType::MintCloseAuthority])?; + msg!(&space.to_string()); + //Create account for the mint and allocate space + create_account( + signer_info, + mint_info, + system_program, + space, + token_program.key, + )?; + + //Initialize the Mint-Close-Authority Extension + //This instruction must come before the instruction to initialize mint data + invoke( + &initialize_mint_close_authority(token_program.key, mint_info.key, Some(signer_info.key))?, + &[mint_info.clone(), signer_info.clone()], + )?; + + initialize_mint( + mint_info, + signer_info, + Some(signer_info), + token_program, + rent_sysvar, + 6, //you can configure it to be passed as instruction data + )?; + + msg!("Mint Close Authority Extension: Initialized!"); + + Ok(()) +} diff --git a/tokens/token-2022/mint-close-authority/steel/program/src/lib.rs b/tokens/token-2022/mint-close-authority/steel/program/src/lib.rs new file mode 100644 index 00000000..ebb08190 --- /dev/null +++ b/tokens/token-2022/mint-close-authority/steel/program/src/lib.rs @@ -0,0 +1,25 @@ +mod initialize; +mod mint_close; + +use initialize::*; +use mint_close::*; + +use steel::*; +use steel_api::prelude::*; + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let (ix, data) = parse_instruction(&steel_api::ID, program_id, data)?; + + match ix { + SteelInstruction::Initialize => process_initialize(accounts, data)?, + SteelInstruction::MintCloseAuthority => process_mint_close(accounts, data)?, + } + + Ok(()) +} + +entrypoint!(process_instruction); diff --git a/tokens/token-2022/mint-close-authority/steel/program/src/mint_close.rs b/tokens/token-2022/mint-close-authority/steel/program/src/mint_close.rs new file mode 100644 index 00000000..f5c8a6b5 --- /dev/null +++ b/tokens/token-2022/mint-close-authority/steel/program/src/mint_close.rs @@ -0,0 +1,31 @@ +use solana_program::{msg, program::invoke}; +use spl_token_2022::instruction::close_account; +use steel::*; + +pub fn process_mint_close(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { + // Load accounts. + let [signer_info, mint_info, destination_info, system_program, token_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + signer_info.is_signer()?; + mint_info.is_signer()?; + destination_info.is_writable()?; + token_program.is_program(&spl_token_2022::ID)?; + system_program.is_program(&system_program::ID)?; + + invoke( + &close_account( + token_program.key, + mint_info.key, + destination_info.key, + signer_info.key, + &[], + )?, + &[mint_info.clone(), destination_info.clone(), signer_info.clone()], + )?; + + msg!("Mint Close Authority Extension: Account Closed!"); + + Ok(()) +} diff --git a/tokens/token-2022/mint-close-authority/steel/program/tests/test.rs b/tokens/token-2022/mint-close-authority/steel/program/tests/test.rs new file mode 100644 index 00000000..7050c650 --- /dev/null +++ b/tokens/token-2022/mint-close-authority/steel/program/tests/test.rs @@ -0,0 +1,81 @@ +use solana_program::hash::Hash; +use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_sdk::{signature::Keypair, signer::Signer, transaction::Transaction}; +use spl_token_2022::{ + extension::{ + mint_close_authority::MintCloseAuthority, + BaseStateWithExtensions, ExtensionType, StateWithExtensions, + }, + state::Mint, +}; + +use steel_api::prelude::*; + +async fn setup() -> (BanksClient, Keypair, Hash) { + let mut program_test = ProgramTest::new( + "steel_program", + steel_api::ID, + processor!(steel_program::process_instruction), + ); + program_test.prefer_bpf(true); + program_test.start().await +} + +#[tokio::test] +async fn run_test() { + // Setup test + let (mut banks, payer, blockhash) = setup().await; + let recv = Keypair::new(); + let mint = Keypair::new(); + + // Submit initialize transaction. + let ix = initialize(payer.pubkey(), mint.pubkey()); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &mint], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + //Get mint data + let mint_account = banks.get_account(mint.pubkey()).await.unwrap().unwrap(); + dbg!(mint_account.lamports); + let mint_data = mint_account.data.as_slice(); + let state_with_extensions = StateWithExtensions::::unpack(mint_data).unwrap(); + let extension_types = state_with_extensions.get_extension_types().unwrap(); + let extension = state_with_extensions + .get_extension::() + .unwrap(); + + assert_eq!( + extension.close_authority.0, + payer.pubkey(), + "Mint close authority mismatch" + ); + + assert!( + extension_types.contains(&ExtensionType::MintCloseAuthority), + "Mint Close Authority extension not found in mint account" + ); + + // Submit close mint transaction. + let ix = mint_close(payer.pubkey(), recv.pubkey(), mint.pubkey()); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &mint], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + //Get mint data + let mint_account = banks.get_account(mint.pubkey()).await.unwrap(); + assert!(mint_account.is_none(), "Mint account should be closed but still exists"); + + //Checking if the rent was transferred to the destination account + let recv_account = banks.get_account(recv.pubkey()).await.unwrap().unwrap(); + assert!(recv_account.lamports == 2296800); +} diff --git a/tokens/token-2022/non-transferable/steel/.gitignore b/tokens/token-2022/non-transferable/steel/.gitignore new file mode 100644 index 00000000..052739db --- /dev/null +++ b/tokens/token-2022/non-transferable/steel/.gitignore @@ -0,0 +1,2 @@ +target +test-ledger diff --git a/tokens/token-2022/non-transferable/steel/Cargo.toml b/tokens/token-2022/non-transferable/steel/Cargo.toml new file mode 100644 index 00000000..ae0a6d27 --- /dev/null +++ b/tokens/token-2022/non-transferable/steel/Cargo.toml @@ -0,0 +1,22 @@ +[workspace] +resolver = "2" +members = ["api", "program"] + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +homepage = "" +documentation = "" +repository = "" +readme = "./README.md" +keywords = ["solana"] + +[workspace.dependencies] +steel-api = { path = "./api", version = "0.1.0" } +bytemuck = "1.14" +num_enum = "0.7" +solana-program = "2.1" +steel = "4.0" +thiserror = "1.0" +spl-token-2022 = {version = "7.0.0", features = ["no-entrypoint"]} \ No newline at end of file diff --git a/tokens/token-2022/non-transferable/steel/README.md b/tokens/token-2022/non-transferable/steel/README.md new file mode 100644 index 00000000..cfc508fd --- /dev/null +++ b/tokens/token-2022/non-transferable/steel/README.md @@ -0,0 +1,28 @@ +# Steel + +**Steel** is a ... + +## API +- [`Consts`](api/src/consts.rs) – Program constants. +- [`Error`](api/src/error.rs) – Custom program errors. +- [`Event`](api/src/event.rs) – Custom program events. +- [`Instruction`](api/src/instruction.rs) – Declared instructions. + +## Instructions +- [`Add`](program/src/add.rs) – Add ... +- [`Initialize`](program/src/initialize.rs) – Initialize ... + +## State +- [`Counter`](api/src/state/counter.rs) – Counter ... + +## Get started + +Compile your program: +```sh +steel build +``` + +Run unit and integration tests: +```sh +steel test +``` diff --git a/tokens/token-2022/non-transferable/steel/api/Cargo.toml b/tokens/token-2022/non-transferable/steel/api/Cargo.toml new file mode 100644 index 00000000..e514258e --- /dev/null +++ b/tokens/token-2022/non-transferable/steel/api/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "steel-api" +description = "API for interacting with the Steel program" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true + +[dependencies] +bytemuck.workspace = true +num_enum.workspace = true +solana-program.workspace = true +steel.workspace = true +thiserror.workspace = true +spl-token-2022.workspace = true diff --git a/tokens/token-2022/non-transferable/steel/api/src/error.rs b/tokens/token-2022/non-transferable/steel/api/src/error.rs new file mode 100644 index 00000000..96e84da4 --- /dev/null +++ b/tokens/token-2022/non-transferable/steel/api/src/error.rs @@ -0,0 +1,10 @@ +use steel::*; + +#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, IntoPrimitive)] +#[repr(u32)] +pub enum SteelError { + #[error("This is a dummy error")] + Dummy = 0, +} + +error!(SteelError); diff --git a/tokens/token-2022/non-transferable/steel/api/src/instruction.rs b/tokens/token-2022/non-transferable/steel/api/src/instruction.rs new file mode 100644 index 00000000..c29bdee0 --- /dev/null +++ b/tokens/token-2022/non-transferable/steel/api/src/instruction.rs @@ -0,0 +1,13 @@ +use steel::*; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] +pub enum SteelInstruction { + Initialize = 0, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct Initialize {} + +instruction!(SteelInstruction, Initialize); diff --git a/tokens/token-2022/non-transferable/steel/api/src/lib.rs b/tokens/token-2022/non-transferable/steel/api/src/lib.rs new file mode 100644 index 00000000..2c146dac --- /dev/null +++ b/tokens/token-2022/non-transferable/steel/api/src/lib.rs @@ -0,0 +1,14 @@ +pub mod error; +pub mod instruction; +pub mod sdk; + +pub mod prelude { + pub use crate::error::*; + pub use crate::instruction::*; + pub use crate::sdk::*; +} + +use steel::*; + +// TODO Set program id +declare_id!("z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35"); diff --git a/tokens/token-2022/non-transferable/steel/api/src/sdk.rs b/tokens/token-2022/non-transferable/steel/api/src/sdk.rs new file mode 100644 index 00000000..c813b5cc --- /dev/null +++ b/tokens/token-2022/non-transferable/steel/api/src/sdk.rs @@ -0,0 +1,17 @@ +use steel::*; + +use crate::prelude::*; + +pub fn initialize(signer: Pubkey, mint: Pubkey) -> Instruction { + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(signer, true), + AccountMeta::new(mint, true), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(spl_token_2022::ID, false), + AccountMeta::new_readonly(sysvar::rent::ID, false), + ], + data: Initialize {}.to_bytes(), + } +} diff --git a/tokens/token-2022/non-transferable/steel/program/Cargo.toml b/tokens/token-2022/non-transferable/steel/program/Cargo.toml new file mode 100644 index 00000000..90ef6655 --- /dev/null +++ b/tokens/token-2022/non-transferable/steel/program/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "steel-program" +description = "" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +steel-api.workspace = true +solana-program.workspace = true +steel.workspace = true +spl-token-2022.workspace = true + +[dev-dependencies] +base64 = "0.21" +rand = "0.8.5" +solana-program-test = "2.1" +solana-sdk = "2.1" +tokio = { version = "1.35", features = ["full"] } +spl-associated-token-account = {version = "7.0.0", features = ["no-entrypoint"]} + diff --git a/tokens/token-2022/non-transferable/steel/program/src/initialize.rs b/tokens/token-2022/non-transferable/steel/program/src/initialize.rs new file mode 100644 index 00000000..eb5a33dc --- /dev/null +++ b/tokens/token-2022/non-transferable/steel/program/src/initialize.rs @@ -0,0 +1,53 @@ +use solana_program::{msg, program::invoke}; +use spl_token_2022::{ + extension::ExtensionType, instruction::initialize_non_transferable_mint, state::Mint, +}; +use steel::*; + +pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { + // Load accounts. + let [signer_info, mint_info, system_program, token_program, rent_sysvar] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + msg!("Account loaded"); + + //Validation + signer_info.is_signer()?; + mint_info.is_signer()?.is_empty()?.is_writable()?; + token_program.is_program(&spl_token_2022::ID)?; + system_program.is_program(&system_program::ID)?; + rent_sysvar.is_sysvar(&sysvar::rent::ID)?; + + //Calculate space for mint and extension data + let space = + ExtensionType::try_calculate_account_len::(&[ExtensionType::NonTransferable])?; + + //Create account for the mint and allocate space + create_account( + signer_info, + mint_info, + system_program, + space, + token_program.key, + )?; + + //Initialize the non transferable extension + //This instruction must come before the instruction to initialize mint data + invoke( + &initialize_non_transferable_mint(token_program.key, mint_info.key)?, + &[mint_info.clone()], + )?; + + initialize_mint( + mint_info, + signer_info, + Some(signer_info), + token_program, + rent_sysvar, + 6, //you can always pass this as instruction data + )?; + + msg!("Non Transferable Extension: Initialized."); + + Ok(()) +} diff --git a/tokens/token-2022/non-transferable/steel/program/src/lib.rs b/tokens/token-2022/non-transferable/steel/program/src/lib.rs new file mode 100644 index 00000000..73785439 --- /dev/null +++ b/tokens/token-2022/non-transferable/steel/program/src/lib.rs @@ -0,0 +1,22 @@ +mod initialize; + +use initialize::*; + +use steel::*; +use steel_api::prelude::*; + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let (ix, data) = parse_instruction(&steel_api::ID, program_id, data)?; + + match ix { + SteelInstruction::Initialize => process_initialize(accounts, data)?, + } + + Ok(()) +} + +entrypoint!(process_instruction); diff --git a/tokens/token-2022/non-transferable/steel/program/tests/test.rs b/tokens/token-2022/non-transferable/steel/program/tests/test.rs new file mode 100644 index 00000000..d09ac4e6 --- /dev/null +++ b/tokens/token-2022/non-transferable/steel/program/tests/test.rs @@ -0,0 +1,111 @@ +use solana_program::hash::Hash; +use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_sdk::{signature::Keypair, signer::Signer, transaction::Transaction}; +use spl_associated_token_account::{ + get_associated_token_address_with_program_id, instruction::create_associated_token_account, +}; +use spl_token_2022::instruction::{mint_to, transfer_checked}; +use steel_api::prelude::*; + +async fn setup() -> (BanksClient, Keypair, Hash) { + let mut program_test = ProgramTest::new( + "steel_program", + steel_api::ID, + processor!(steel_program::process_instruction), + ); + program_test.prefer_bpf(true); + program_test.start().await +} + +#[tokio::test] +async fn run_test() { + // Setup test + let (mut banks, payer, blockhash) = setup().await; + + let mint = Keypair::new(); + let receiver = Keypair::new(); + + // Submit initialize transaction. + let ix = initialize(payer.pubkey(), mint.pubkey()); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &mint], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + //create token accounts + let create_token_acc_ix1 = create_associated_token_account( + &payer.pubkey(), + &payer.pubkey(), + &mint.pubkey(), + &spl_token_2022::ID, + ); + let create_token_acc_ix2 = create_associated_token_account( + &payer.pubkey(), + &receiver.pubkey(), + &mint.pubkey(), + &spl_token_2022::ID, + ); + + // Derive ATAs + let payer_ata = get_associated_token_address_with_program_id( + &payer.pubkey(), + &mint.pubkey(), + &spl_token_2022::ID, + ); + + let receiver_ata = get_associated_token_address_with_program_id( + &receiver.pubkey(), + &mint.pubkey(), + &spl_token_2022::ID, + ); + + //Mint Tokens + let mint_ix = mint_to( + &spl_token_2022::ID, + &mint.pubkey(), + &payer_ata, + &payer.pubkey(), + &[], + 1_000_000_000, + ) + .unwrap(); + + let transfer_ix = transfer_checked( + &spl_token_2022::ID, + &payer_ata, + &mint.pubkey(), + &receiver_ata, + &payer.pubkey(), + &[], + 1_000_000, + 6, + ) + .unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ + create_token_acc_ix1, + create_token_acc_ix2, + mint_ix, + transfer_ix, + ], + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); + + let res = banks.process_transaction(tx).await; + assert!(res.is_err(), "Transfer unexpectedly succeeded"); + + let err_string = format!("{:?}", res); + + // Custom(0x25) = Custom(37) = TokenError::NonTransferable + assert!( + err_string.contains("Custom(37)"), + "Expected TokenError::NonTransferable (37), got: {}", + err_string + ); +} diff --git a/tokens/token-2022/permanent-delegate/steel/.gitignore b/tokens/token-2022/permanent-delegate/steel/.gitignore new file mode 100644 index 00000000..052739db --- /dev/null +++ b/tokens/token-2022/permanent-delegate/steel/.gitignore @@ -0,0 +1,2 @@ +target +test-ledger diff --git a/tokens/token-2022/permanent-delegate/steel/Cargo.toml b/tokens/token-2022/permanent-delegate/steel/Cargo.toml new file mode 100644 index 00000000..23402b9a --- /dev/null +++ b/tokens/token-2022/permanent-delegate/steel/Cargo.toml @@ -0,0 +1,22 @@ +[workspace] +resolver = "2" +members = ["api", "program"] + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +homepage = "" +documentation = "" +repository = "" +readme = "./README.md" +keywords = ["solana"] + +[workspace.dependencies] +steel-api = { path = "./api", version = "0.1.0" } +bytemuck = "1.14" +num_enum = "0.7" +solana-program = "2.1" +steel = "4.0" +thiserror = "1.0" +spl-token-2022 = {version = "7.0.0", features = ["no-entrypoint"]} diff --git a/tokens/token-2022/permanent-delegate/steel/README.md b/tokens/token-2022/permanent-delegate/steel/README.md new file mode 100644 index 00000000..cfc508fd --- /dev/null +++ b/tokens/token-2022/permanent-delegate/steel/README.md @@ -0,0 +1,28 @@ +# Steel + +**Steel** is a ... + +## API +- [`Consts`](api/src/consts.rs) – Program constants. +- [`Error`](api/src/error.rs) – Custom program errors. +- [`Event`](api/src/event.rs) – Custom program events. +- [`Instruction`](api/src/instruction.rs) – Declared instructions. + +## Instructions +- [`Add`](program/src/add.rs) – Add ... +- [`Initialize`](program/src/initialize.rs) – Initialize ... + +## State +- [`Counter`](api/src/state/counter.rs) – Counter ... + +## Get started + +Compile your program: +```sh +steel build +``` + +Run unit and integration tests: +```sh +steel test +``` diff --git a/tokens/token-2022/permanent-delegate/steel/api/Cargo.toml b/tokens/token-2022/permanent-delegate/steel/api/Cargo.toml new file mode 100644 index 00000000..e514258e --- /dev/null +++ b/tokens/token-2022/permanent-delegate/steel/api/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "steel-api" +description = "API for interacting with the Steel program" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true + +[dependencies] +bytemuck.workspace = true +num_enum.workspace = true +solana-program.workspace = true +steel.workspace = true +thiserror.workspace = true +spl-token-2022.workspace = true diff --git a/tokens/token-2022/permanent-delegate/steel/api/src/error.rs b/tokens/token-2022/permanent-delegate/steel/api/src/error.rs new file mode 100644 index 00000000..96e84da4 --- /dev/null +++ b/tokens/token-2022/permanent-delegate/steel/api/src/error.rs @@ -0,0 +1,10 @@ +use steel::*; + +#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, IntoPrimitive)] +#[repr(u32)] +pub enum SteelError { + #[error("This is a dummy error")] + Dummy = 0, +} + +error!(SteelError); diff --git a/tokens/token-2022/permanent-delegate/steel/api/src/instruction.rs b/tokens/token-2022/permanent-delegate/steel/api/src/instruction.rs new file mode 100644 index 00000000..c29bdee0 --- /dev/null +++ b/tokens/token-2022/permanent-delegate/steel/api/src/instruction.rs @@ -0,0 +1,13 @@ +use steel::*; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] +pub enum SteelInstruction { + Initialize = 0, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct Initialize {} + +instruction!(SteelInstruction, Initialize); diff --git a/tokens/token-2022/permanent-delegate/steel/api/src/lib.rs b/tokens/token-2022/permanent-delegate/steel/api/src/lib.rs new file mode 100644 index 00000000..2c146dac --- /dev/null +++ b/tokens/token-2022/permanent-delegate/steel/api/src/lib.rs @@ -0,0 +1,14 @@ +pub mod error; +pub mod instruction; +pub mod sdk; + +pub mod prelude { + pub use crate::error::*; + pub use crate::instruction::*; + pub use crate::sdk::*; +} + +use steel::*; + +// TODO Set program id +declare_id!("z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35"); diff --git a/tokens/token-2022/permanent-delegate/steel/api/src/sdk.rs b/tokens/token-2022/permanent-delegate/steel/api/src/sdk.rs new file mode 100644 index 00000000..5a0025a4 --- /dev/null +++ b/tokens/token-2022/permanent-delegate/steel/api/src/sdk.rs @@ -0,0 +1,18 @@ +use steel::*; + +use crate::prelude::*; + +pub fn initialize(signer: Pubkey, mint: Pubkey, delegate: Pubkey) -> Instruction { + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(signer, true), + AccountMeta::new(mint, true), + AccountMeta::new(delegate, false), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(spl_token_2022::ID, false), + AccountMeta::new_readonly(sysvar::rent::ID, false), + ], + data: Initialize {}.to_bytes(), + } +} diff --git a/tokens/token-2022/permanent-delegate/steel/program/Cargo.toml b/tokens/token-2022/permanent-delegate/steel/program/Cargo.toml new file mode 100644 index 00000000..dc46ef20 --- /dev/null +++ b/tokens/token-2022/permanent-delegate/steel/program/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "steel-program" +description = "" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +steel-api.workspace = true +solana-program.workspace = true +steel.workspace = true +spl-token-2022.workspace = true + +[dev-dependencies] +base64 = "0.21" +rand = "0.8.5" +solana-program-test = "2.1" +solana-sdk = "2.1" +tokio = { version = "1.35", features = ["full"] } +spl-associated-token-account = {version = "7.0.0", features = ["no-entrypoint"]} + + diff --git a/tokens/token-2022/permanent-delegate/steel/program/src/initialize.rs b/tokens/token-2022/permanent-delegate/steel/program/src/initialize.rs new file mode 100644 index 00000000..9b7eee00 --- /dev/null +++ b/tokens/token-2022/permanent-delegate/steel/program/src/initialize.rs @@ -0,0 +1,55 @@ +use solana_program::{msg, program::invoke}; +use spl_token_2022::{ + extension::ExtensionType, instruction::initialize_permanent_delegate, state::Mint, +}; +use steel::*; + +pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { + // Load accounts. + let [signer_info, mint_info, delegate_info, system_program, token_program, rent_sysvar] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + msg!("Account loaded"); + + //Validation + signer_info.is_signer()?; + mint_info.is_signer()?.is_empty()?.is_writable()?; + token_program.is_program(&spl_token_2022::ID)?; + system_program.is_program(&system_program::ID)?; + rent_sysvar.is_sysvar(&sysvar::rent::ID)?; + + //Calculate space for mint and extension data + let space = + ExtensionType::try_calculate_account_len::(&[ExtensionType::PermanentDelegate])?; + + //Create account for the mint and allocate space + create_account( + signer_info, + mint_info, + system_program, + space, + token_program.key, + )?; + + //Initialize the permanent delegate extension + //This instruction must come before the instruction to initialize mint data + invoke( + &initialize_permanent_delegate(token_program.key, mint_info.key, delegate_info.key)?, + &[mint_info.clone()], + )?; + + initialize_mint( + mint_info, + signer_info, + Some(signer_info), + token_program, + rent_sysvar, + 6, + )?; + + msg!("Permanent Delegate Extension: Initialized."); + + Ok(()) +} diff --git a/tokens/token-2022/permanent-delegate/steel/program/src/lib.rs b/tokens/token-2022/permanent-delegate/steel/program/src/lib.rs new file mode 100644 index 00000000..73785439 --- /dev/null +++ b/tokens/token-2022/permanent-delegate/steel/program/src/lib.rs @@ -0,0 +1,22 @@ +mod initialize; + +use initialize::*; + +use steel::*; +use steel_api::prelude::*; + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let (ix, data) = parse_instruction(&steel_api::ID, program_id, data)?; + + match ix { + SteelInstruction::Initialize => process_initialize(accounts, data)?, + } + + Ok(()) +} + +entrypoint!(process_instruction); diff --git a/tokens/token-2022/permanent-delegate/steel/program/tests/test.rs b/tokens/token-2022/permanent-delegate/steel/program/tests/test.rs new file mode 100644 index 00000000..1c596b6a --- /dev/null +++ b/tokens/token-2022/permanent-delegate/steel/program/tests/test.rs @@ -0,0 +1,84 @@ +use solana_program::hash::Hash; +use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_sdk::{signature::Keypair, signer::Signer, transaction::Transaction}; +use spl_associated_token_account::{ + get_associated_token_address_with_program_id, instruction::create_associated_token_account, +}; +use spl_token_2022::instruction::{burn_checked, mint_to}; +use steel::*; +use steel_api::prelude::*; + +async fn setup() -> (BanksClient, Keypair, Hash) { + let mut program_test = ProgramTest::new( + "steel_program", + steel_api::ID, + processor!(steel_program::process_instruction), + ); + program_test.prefer_bpf(true); + program_test.start().await +} + +#[tokio::test] +async fn run_test() { + // Setup test + let (mut banks, payer, blockhash) = setup().await; + let mint = Keypair::new(); + let delegate = Keypair::new(); + + // Submit initialize transaction. + let ix = initialize(payer.pubkey(), mint.pubkey(), delegate.pubkey()); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &mint], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + //Create Token Account, Mint Tokens + //Burn as Permanent Delegate + //Using a new/random keypair to experiment + let receiver = Keypair::new(); + + let create_token_acc_ix = create_associated_token_account( + &payer.pubkey(), + &receiver.pubkey(), + &mint.pubkey(), + &spl_token_2022::ID, + ); + let receiver_ata = get_associated_token_address_with_program_id( + &receiver.pubkey(), + &mint.pubkey(), + &spl_token_2022::ID, + ); + let mint_ix = mint_to( + &spl_token_2022::ID, + &mint.pubkey(), + &receiver_ata, + &payer.pubkey(), + &[], + 1000, + ) + .unwrap(); + + let burn_ix = burn_checked( + &spl_token_2022::ID, + &receiver_ata, + &mint.pubkey(), + &delegate.pubkey(), + &[], + 1000, + 6, + ) + .unwrap(); + + let tx = Transaction::new_signed_with_payer( + &[create_token_acc_ix, mint_ix, burn_ix], + Some(&payer.pubkey()), + &[&payer, &delegate], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); +} diff --git a/tokens/token-2022/transfer-fee/steel/Cargo.toml b/tokens/token-2022/transfer-fee/steel/Cargo.toml new file mode 100644 index 00000000..70771a85 --- /dev/null +++ b/tokens/token-2022/transfer-fee/steel/Cargo.toml @@ -0,0 +1,23 @@ +[workspace] +resolver = "2" +members = ["api", "program"] + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +homepage = "" +documentation = "" +repository = "" +readme = "./README.md" +keywords = ["solana"] + +[workspace.dependencies] +steel-api = { path = "./api", version = "0.1.0" } +bytemuck = "1.14" +num_enum = "0.7" +solana-program = "2.1" +steel = "4.0" +thiserror = "1.0" +spl-token-2022 = {version = "7.0.0", features = ["no-entrypoint"]} +spl-associated-token-account = {version = "7.0.0", features = ["no-entrypoint"]} \ No newline at end of file diff --git a/tokens/token-2022/transfer-fee/steel/README.md b/tokens/token-2022/transfer-fee/steel/README.md new file mode 100644 index 00000000..cfc508fd --- /dev/null +++ b/tokens/token-2022/transfer-fee/steel/README.md @@ -0,0 +1,28 @@ +# Steel + +**Steel** is a ... + +## API +- [`Consts`](api/src/consts.rs) – Program constants. +- [`Error`](api/src/error.rs) – Custom program errors. +- [`Event`](api/src/event.rs) – Custom program events. +- [`Instruction`](api/src/instruction.rs) – Declared instructions. + +## Instructions +- [`Add`](program/src/add.rs) – Add ... +- [`Initialize`](program/src/initialize.rs) – Initialize ... + +## State +- [`Counter`](api/src/state/counter.rs) – Counter ... + +## Get started + +Compile your program: +```sh +steel build +``` + +Run unit and integration tests: +```sh +steel test +``` diff --git a/tokens/token-2022/transfer-fee/steel/api/Cargo.toml b/tokens/token-2022/transfer-fee/steel/api/Cargo.toml new file mode 100644 index 00000000..722c4895 --- /dev/null +++ b/tokens/token-2022/transfer-fee/steel/api/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "steel-api" +description = "API for interacting with the Steel program" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true + +[dependencies] +bytemuck.workspace = true +num_enum.workspace = true +solana-program.workspace = true +steel.workspace = true +thiserror.workspace = true +spl-token-2022.workspace = true +spl-associated-token-account.workspace = true diff --git a/tokens/token-2022/transfer-fee/steel/api/src/error.rs b/tokens/token-2022/transfer-fee/steel/api/src/error.rs new file mode 100644 index 00000000..96e84da4 --- /dev/null +++ b/tokens/token-2022/transfer-fee/steel/api/src/error.rs @@ -0,0 +1,10 @@ +use steel::*; + +#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, IntoPrimitive)] +#[repr(u32)] +pub enum SteelError { + #[error("This is a dummy error")] + Dummy = 0, +} + +error!(SteelError); diff --git a/tokens/token-2022/transfer-fee/steel/api/src/instruction.rs b/tokens/token-2022/transfer-fee/steel/api/src/instruction.rs new file mode 100644 index 00000000..6d10a5ba --- /dev/null +++ b/tokens/token-2022/transfer-fee/steel/api/src/instruction.rs @@ -0,0 +1,48 @@ +use steel::*; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] +pub enum SteelInstruction { + Initialize = 0, + Transfer = 1, + Harvest = 2, + Withdraw = 3, + UpdateFee = 4, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct Initialize { + pub maximum_fee: [u8; 8], //NOTE: take into consideration the token decimals, you can also scale the input to match the decimals + pub transfer_fee_basis_points: [u8; 2], + pub decimals: u8, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct Transfer { + pub amount: [u8; 8], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct Harvest {} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct Withdraw { + pub destination: [u8; 32], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct UpdateFee { + pub maximum_fee: [u8; 8], //NOTE: take into consideration the token decimals, you can also scale the input to match the decimals. + pub transfer_fee_basis_points: [u8; 2], +} + +instruction!(SteelInstruction, Initialize); +instruction!(SteelInstruction, Transfer); +instruction!(SteelInstruction, Harvest); +instruction!(SteelInstruction, Withdraw); +instruction!(SteelInstruction, UpdateFee); diff --git a/tokens/token-2022/transfer-fee/steel/api/src/lib.rs b/tokens/token-2022/transfer-fee/steel/api/src/lib.rs new file mode 100644 index 00000000..2c146dac --- /dev/null +++ b/tokens/token-2022/transfer-fee/steel/api/src/lib.rs @@ -0,0 +1,14 @@ +pub mod error; +pub mod instruction; +pub mod sdk; + +pub mod prelude { + pub use crate::error::*; + pub use crate::instruction::*; + pub use crate::sdk::*; +} + +use steel::*; + +// TODO Set program id +declare_id!("z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35"); diff --git a/tokens/token-2022/transfer-fee/steel/api/src/sdk.rs b/tokens/token-2022/transfer-fee/steel/api/src/sdk.rs new file mode 100644 index 00000000..fc313638 --- /dev/null +++ b/tokens/token-2022/transfer-fee/steel/api/src/sdk.rs @@ -0,0 +1,111 @@ +use steel::*; + +use crate::prelude::*; + +pub fn initialize( + signer: Pubkey, + mint: Pubkey, + maximum_fee: u64, + transfer_fee_basis_points: u16, + decimals: u8, +) -> Instruction { + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(signer, true), + AccountMeta::new(mint, true), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(spl_token_2022::ID, false), + AccountMeta::new_readonly(sysvar::rent::ID, false), + ], + data: Initialize { + maximum_fee: maximum_fee.to_le_bytes(), + transfer_fee_basis_points: transfer_fee_basis_points.to_le_bytes(), + decimals, + } + .to_bytes(), + } +} + +pub fn transfer( + amount: u64, + signer: Pubkey, + mint: Pubkey, + source_token_account: Pubkey, + destination_token_account: Pubkey, +) -> Instruction { + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(signer, true), + AccountMeta::new_readonly(mint, false), + AccountMeta::new(source_token_account, false), + AccountMeta::new(destination_token_account, false), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(spl_token_2022::ID, false), + ], + data: Transfer { + amount: amount.to_le_bytes(), + } + .to_bytes(), + } +} + +pub fn harvest( + signer: Pubkey, + mint: Pubkey, + harvest_acc_1: Pubkey, + harvest_acc_2: Pubkey, +) -> Instruction { + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(signer, true), + AccountMeta::new(mint, false), + AccountMeta::new(harvest_acc_1, false), + AccountMeta::new(harvest_acc_2, false), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(spl_token_2022::ID, false), + ], + data: Harvest {}.to_bytes(), + } +} + +pub fn withdraw(signer: Pubkey, mint: Pubkey, destination: Pubkey) -> Instruction { + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(signer, true), + AccountMeta::new(mint, false), + AccountMeta::new(destination, false), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(spl_token_2022::ID, false), + ], + data: Withdraw { + destination: destination.to_bytes(), + } + .to_bytes(), + } +} + +pub fn update_fee( + signer: Pubkey, + mint: Pubkey, + maximum_fee: u64, + transfer_fee_basis_points: u16, +) -> Instruction { + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(signer, true), + AccountMeta::new(mint, false), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(spl_token_2022::ID, false), + ], + data: UpdateFee { + maximum_fee: maximum_fee.to_le_bytes(), + transfer_fee_basis_points: transfer_fee_basis_points.to_le_bytes(), + } + .to_bytes(), + } +} diff --git a/tokens/token-2022/transfer-fee/steel/program/Cargo.toml b/tokens/token-2022/transfer-fee/steel/program/Cargo.toml new file mode 100644 index 00000000..c5b6f877 --- /dev/null +++ b/tokens/token-2022/transfer-fee/steel/program/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "steel-program" +description = "" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +steel-api.workspace = true +solana-program.workspace = true +steel.workspace = true +spl-token-2022.workspace = true +spl-associated-token-account.workspace = true + +[dev-dependencies] +base64 = "0.21" +rand = "0.8.5" +solana-program-test = "2.1" +solana-sdk = "2.1" +tokio = { version = "1.35", features = ["full"] } +solana-keypair = "2.2" +solana-transaction = "2.2" +solana-signer = "2.2" diff --git a/tokens/token-2022/transfer-fee/steel/program/src/harvest.rs b/tokens/token-2022/transfer-fee/steel/program/src/harvest.rs new file mode 100644 index 00000000..65218559 --- /dev/null +++ b/tokens/token-2022/transfer-fee/steel/program/src/harvest.rs @@ -0,0 +1,36 @@ +use solana_program::{msg, program::invoke}; +use spl_token_2022::extension::transfer_fee::instruction::harvest_withheld_tokens_to_mint; +use steel::*; + +pub fn process_harvest(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { + //Load accounts + let [signer_info, mint_info, harvest_acc_1_info, harvest_acc_2_info, system_program, token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + signer_info.is_signer()?; + mint_info.is_writable()?; + token_program.is_program(&spl_token_2022::ID)?; + system_program.is_program(&system_program::ID)?; + harvest_acc_1_info.is_writable()?; + harvest_acc_2_info.is_writable()?; + + invoke( + &harvest_withheld_tokens_to_mint( + token_program.key, + mint_info.key, + &[&harvest_acc_1_info.key, &harvest_acc_2_info.key], + )?, + &[ + mint_info.clone(), + harvest_acc_1_info.clone(), + harvest_acc_2_info.clone(), + ], + )?; + + msg!("Transfer Fee Extension: Harvest successful"); + + Ok(()) +} diff --git a/tokens/token-2022/transfer-fee/steel/program/src/initialize.rs b/tokens/token-2022/transfer-fee/steel/program/src/initialize.rs new file mode 100644 index 00000000..57fe11e9 --- /dev/null +++ b/tokens/token-2022/transfer-fee/steel/program/src/initialize.rs @@ -0,0 +1,67 @@ +use solana_program::{msg, program::invoke}; +use spl_token_2022::{ + extension::{transfer_fee::instruction::initialize_transfer_fee_config, ExtensionType}, + state::Mint, +}; +use steel::*; +use steel_api::prelude::*; + +pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { + let args = Initialize::try_from_bytes(&_data)?; + let maximum_fee = u64::from_le_bytes(args.maximum_fee); + let transfer_fee_basis_points = u16::from_le_bytes(args.transfer_fee_basis_points); + msg!("Parsed arguments"); + + // Load accounts. + let [signer_info, mint_info, system_program, token_program, rent_sysvar] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + msg!("Account loaded"); + + //Validation + signer_info.is_signer()?; + mint_info.is_signer()?.is_empty()?.is_writable()?; + token_program.is_program(&spl_token_2022::ID)?; + system_program.is_program(&system_program::ID)?; + rent_sysvar.is_sysvar(&sysvar::rent::ID)?; + + //Calculate space for mint and extension data + let space = + ExtensionType::try_calculate_account_len::(&[ExtensionType::TransferFeeConfig])?; + + //Create account for the mint and allocate space + create_account( + signer_info, + mint_info, + system_program, + space, + token_program.key, + )?; + + //Initialize the transfer fee extension + //This instruction must come before the instruction to initialize mint data + invoke( + &initialize_transfer_fee_config( + token_program.key, + mint_info.key, + Some(signer_info.key), + Some(signer_info.key), + transfer_fee_basis_points, + maximum_fee, + )?, + &[mint_info.clone()], + )?; + + initialize_mint( + mint_info, + signer_info, + Some(signer_info), + token_program, + rent_sysvar, + args.decimals, + )?; + + msg!("Transfer Fee Extension: Initialized."); + + Ok(()) +} diff --git a/tokens/token-2022/transfer-fee/steel/program/src/lib.rs b/tokens/token-2022/transfer-fee/steel/program/src/lib.rs new file mode 100644 index 00000000..e2f8f9ca --- /dev/null +++ b/tokens/token-2022/transfer-fee/steel/program/src/lib.rs @@ -0,0 +1,34 @@ +mod harvest; +mod initialize; +mod transfer; +mod update_fee; +mod withdraw; + +use harvest::*; +use initialize::*; +use transfer::*; +use update_fee::*; +use withdraw::*; + +use steel::*; +use steel_api::prelude::*; + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let (ix, data) = parse_instruction(&steel_api::ID, program_id, data)?; + + match ix { + SteelInstruction::Initialize => process_initialize(accounts, data)?, + SteelInstruction::Transfer => process_transfer(accounts, data)?, + SteelInstruction::Harvest => process_harvest(accounts, data)?, + SteelInstruction::Withdraw => process_withdraw(accounts, data)?, + SteelInstruction::UpdateFee => process_update_fee(accounts, data)?, + } + + Ok(()) +} + +entrypoint!(process_instruction); diff --git a/tokens/token-2022/transfer-fee/steel/program/src/transfer.rs b/tokens/token-2022/transfer-fee/steel/program/src/transfer.rs new file mode 100644 index 00000000..0f38c9dd --- /dev/null +++ b/tokens/token-2022/transfer-fee/steel/program/src/transfer.rs @@ -0,0 +1,64 @@ +use solana_program::{msg, program::invoke}; +use spl_token_2022::{ + extension::{ + transfer_fee::{instruction::transfer_checked_with_fee, TransferFeeConfig}, + BaseStateWithExtensions, StateWithExtensions, + }, + state::Mint, +}; +use steel::*; +use steel_api::prelude::*; + +// transfer fees are automatically deducted from the transfer amount +// recipients receives (transfer amount - fees) +// transfer fees are stored directly on the recipient token account and must be "harvested" +pub fn process_transfer(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { + let args = Transfer::try_from_bytes(&_data)?; + let amount = u64::from_le_bytes(args.amount); + + // Load accounts. + let [signer_info, mint_info, source_token_account, destination_token_account, system_program, token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + signer_info.is_signer()?; + source_token_account.is_writable()?; + destination_token_account.is_writable()?; + token_program.is_program(&spl_token_2022::ID)?; + system_program.is_program(&system_program::ID)?; + + //Get mint extension info + let mint_data = mint_info.data.borrow(); + let mint_with_extension = StateWithExtensions::::unpack(&mint_data)?; + msg!(&mint_with_extension.base.decimals.to_string()); + + //calculate expected fee and epoch + let epoch = Clock::get()?.epoch; + let extension_data = mint_with_extension.get_extension::()?; + let fee = extension_data.calculate_epoch_fee(epoch, amount).unwrap(); + + invoke( + &transfer_checked_with_fee( + token_program.key, + source_token_account.key, + mint_info.key, + destination_token_account.key, + signer_info.key, + &[], + amount, + mint_with_extension.base.decimals, + fee, + )?, + &[ + source_token_account.clone(), + mint_info.clone(), + destination_token_account.clone(), + signer_info.clone(), + ], + )?; + + msg!("Transfer Fee Extension: Transfer Successful."); + Ok(()) +} diff --git a/tokens/token-2022/transfer-fee/steel/program/src/update_fee.rs b/tokens/token-2022/transfer-fee/steel/program/src/update_fee.rs new file mode 100644 index 00000000..9e42090e --- /dev/null +++ b/tokens/token-2022/transfer-fee/steel/program/src/update_fee.rs @@ -0,0 +1,38 @@ +use solana_program::{msg, program::invoke}; +use spl_token_2022::extension::transfer_fee::instruction::set_transfer_fee; +use steel::*; +use steel_api::prelude::*; + +// Note that there is a 2 epoch delay from when new fee updates take effect +// This is a safely feature built into the extension +// https://github.com/solana-program/token-2022/blob/main/program/src/extension/transfer_fee/processor.rs#L100 +pub fn process_update_fee(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { + let args = UpdateFee::try_from_bytes(&_data)?; + let maximum_fee = u64::from_le_bytes(args.maximum_fee); + let transfer_fee_basis_points = u16::from_le_bytes(args.transfer_fee_basis_points); + + //Load accounts + let [signer_info, mint_info, system_program, token_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + signer_info.is_signer()?; + mint_info.is_writable()?; + token_program.is_program(&spl_token_2022::ID)?; + system_program.is_program(&system_program::ID)?; + + invoke( + &set_transfer_fee( + token_program.key, + mint_info.key, + signer_info.key, + &[], + transfer_fee_basis_points, + maximum_fee, + )?, + &[mint_info.clone(), signer_info.clone()], + )?; + + msg!("Transfer Fee Extension: Update Transfer Fees Successful"); + Ok(()) +} diff --git a/tokens/token-2022/transfer-fee/steel/program/src/withdraw.rs b/tokens/token-2022/transfer-fee/steel/program/src/withdraw.rs new file mode 100644 index 00000000..9ee12756 --- /dev/null +++ b/tokens/token-2022/transfer-fee/steel/program/src/withdraw.rs @@ -0,0 +1,31 @@ +use solana_program::{msg, program::invoke}; +use spl_token_2022::extension::transfer_fee::instruction::withdraw_withheld_tokens_from_mint; +use steel::*; + +//You can either withdraw tokens from the mint account after "harvesting" or withdraw from the user accounts directly(method: withdraw_withheld_tokens_from_account()). +pub fn process_withdraw(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { + //Load accounts + let [signer_info, mint_info, destination, system_program, token_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + signer_info.is_signer()?; + mint_info.is_writable()?; + destination.is_writable()?; + token_program.is_program(&spl_token_2022::ID)?; + system_program.is_program(&system_program::ID)?; + + invoke( + &withdraw_withheld_tokens_from_mint( + token_program.key, + mint_info.key, + destination.key, + signer_info.key, + &[], + )?, + &[mint_info.clone(), destination.clone(), signer_info.clone()], + )?; + + msg!("Transfer Fee Extension: Withdraw successful"); + Ok(()) +} diff --git a/tokens/token-2022/transfer-fee/steel/program/tests/test.rs b/tokens/token-2022/transfer-fee/steel/program/tests/test.rs new file mode 100644 index 00000000..818adb35 --- /dev/null +++ b/tokens/token-2022/transfer-fee/steel/program/tests/test.rs @@ -0,0 +1,244 @@ +use solana_keypair::Keypair; +use solana_program::hash::Hash; +use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_signer::Signer; +use solana_transaction::Transaction; +use spl_associated_token_account::{ + get_associated_token_address_with_program_id, instruction::create_associated_token_account, +}; +use spl_token_2022::{ + extension::{ + transfer_fee::{TransferFeeAmount, TransferFeeConfig}, + BaseStateWithExtensions, ExtensionType, StateWithExtensions, + }, + instruction::mint_to, + state::{Account, Mint}, +}; +use steel_api::prelude::*; + +async fn setup() -> (BanksClient, Keypair, Hash) { + let mut program_test = ProgramTest::new( + "steel_program", + steel_api::ID, + processor!(steel_program::process_instruction), + ); + //Custom SPL Token 2022 + // program_test.add_program("spl_token_2022", spl_token_2022::ID, None); + program_test.prefer_bpf(true); + program_test.start().await +} + +#[tokio::test] +async fn run_test() { + // Setup test + let (mut banks, sender, blockhash) = setup().await; + + let first_receiver = Keypair::new(); + let second_receiver = Keypair::new(); + + let mint = Keypair::new(); + + let decimals = 6; + let maximum_fee = 500_000; //0.5 Token based on 6 decimals + let transfer_fee_basis_points = 1000; //10% + + // 1. Initialize Transfer Fee Extension + let ix = initialize( + sender.pubkey(), + mint.pubkey(), + maximum_fee, + transfer_fee_basis_points, + decimals, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&sender.pubkey()), + &[&sender, &mint], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + //Get mint data + let mint_account = banks.get_account(mint.pubkey()).await.unwrap().unwrap(); + + let mint_data = mint_account.data.as_slice(); + let state_with_extensions = StateWithExtensions::::unpack(mint_data).unwrap(); + let extension_types = state_with_extensions.get_extension_types().unwrap(); + let extension = state_with_extensions + .get_extension::() + .unwrap(); + + assert_eq!( + extension.transfer_fee_config_authority.0, + sender.pubkey(), + "Transfer fee config authority mismatch" + ); + + assert!( + extension_types.contains(&ExtensionType::TransferFeeConfig), + "TransferFeeConfig extension not found in mint account" + ); + + //Derive sender's ata. + let sender_ata = get_associated_token_address_with_program_id( + &sender.pubkey(), + &mint.pubkey(), + &spl_token_2022::ID, + ); + + //Mint Tokens + let mint_ix = mint_to( + &spl_token_2022::ID, + &mint.pubkey(), + &sender_ata, + &sender.pubkey(), + &[], + 1_000_000_000, + ) + .unwrap(); + + //create token accounts + let create_token_acc_ix1 = create_associated_token_account( + &sender.pubkey(), + &sender.pubkey(), + &mint.pubkey(), + &spl_token_2022::ID, + ); + let create_token_acc_ix2 = create_associated_token_account( + &sender.pubkey(), + &first_receiver.pubkey(), + &mint.pubkey(), + &spl_token_2022::ID, + ); + + let create_token_acc_ix3 = create_associated_token_account( + &sender.pubkey(), + &second_receiver.pubkey(), + &mint.pubkey(), + &spl_token_2022::ID, + ); + + // Derive ATAs + let first_receiver_ata = get_associated_token_address_with_program_id( + &first_receiver.pubkey(), + &mint.pubkey(), + &spl_token_2022::ID, + ); + + let second_receiver_ata = get_associated_token_address_with_program_id( + &second_receiver.pubkey(), + &mint.pubkey(), + &spl_token_2022::ID, + ); + + //2. Transfer with transfer_fee functionality + let ix1 = transfer( + 1_000_000, + sender.pubkey(), + mint.pubkey(), + sender_ata, + first_receiver_ata, + ); + let ix2 = transfer( + 500_000, + sender.pubkey(), + mint.pubkey(), + sender_ata, + second_receiver_ata, + ); + + let tx = Transaction::new_signed_with_payer( + &[ + create_token_acc_ix1, + create_token_acc_ix2, + create_token_acc_ix3, + mint_ix, + ix1, + ix2, + ], + Some(&sender.pubkey()), + &[&sender], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + // Check the withheld amount on receiver accounts for expected transfer fees. + //First receiver + let receiver_acc = banks + .get_account(first_receiver_ata) + .await + .unwrap() + .unwrap(); + + // let receiver_acc_data = receiver_acc.data.as_slice(); + let state_with_extensions = + StateWithExtensions::::unpack(&receiver_acc.data.as_slice()).unwrap(); + let receiver_acc_data = state_with_extensions + .get_extension::() + .unwrap(); + assert!(u64::from(receiver_acc_data.withheld_amount) == 100000); + + //Second receiver + let receiver_acc = banks + .get_account(second_receiver_ata) + .await + .unwrap() + .unwrap(); + + let receiver_token = + StateWithExtensions::::unpack(&receiver_acc.data.as_slice()).unwrap(); + let receiver_acc_data = receiver_token.get_extension::().unwrap(); + assert!(u64::from(receiver_acc_data.withheld_amount) == 50000); + + // 3. Harvest Rewards to Mint account + let ix = harvest( + sender.pubkey(), + mint.pubkey(), + first_receiver_ata, + second_receiver_ata, + ); + + let tx = + Transaction::new_signed_with_payer(&[ix], Some(&sender.pubkey()), &[&sender], blockhash); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + //Check if mint has received the harvested fees. + let mint_account = banks.get_account(mint.pubkey()).await.unwrap().unwrap(); + + let mint_data = mint_account.data.as_slice(); + let state_with_extensions = StateWithExtensions::::unpack(mint_data).unwrap(); + let extension = state_with_extensions + .get_extension::() + .unwrap(); + assert!(u64::from(extension.withheld_amount) == 150000); + + // 4. Withdraw Rewards from Mint account + let ix = withdraw(sender.pubkey(), mint.pubkey(), first_receiver_ata); + let tx = + Transaction::new_signed_with_payer(&[ix], Some(&sender.pubkey()), &[&sender], blockhash); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + //Check if the withdrawn amount is in the account + let acc = banks + .get_account(first_receiver_ata) + .await + .unwrap() + .unwrap(); + + let acc_data = StateWithExtensions::::unpack(&acc.data.as_slice()).unwrap(); + + // 1,050,000 = Initial transfer of 1,000,000 (after 10% fee → 900,000) + // + 150,000 harvested and withdrawn from the mint + assert!(acc_data.base.amount == 1050000); + + // 5. Update Transfer Fees + let ix = update_fee(sender.pubkey(), mint.pubkey(), 200000, 2000); + let tx = + Transaction::new_signed_with_payer(&[ix], Some(&sender.pubkey()), &[&sender], blockhash); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); +}