diff --git a/Cargo.lock b/Cargo.lock index 7c0c426..17526ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2105,6 +2105,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "mpl-token-metadata" +version = "4.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf0f61b553e424a6234af1268456972ee66c2222e1da89079242251fa7479e5" +dependencies = [ + "borsh 0.10.3", + "num-derive 0.3.3", + "num-traits", + "solana-program", + "thiserror", +] + [[package]] name = "nix" version = "0.26.4" @@ -2358,6 +2371,7 @@ dependencies = [ "bs58 0.5.0", "bs64", "bytemuck", + "mpl-token-metadata", "num_enum 0.7.2", "rand 0.8.5", "shank", diff --git a/Cargo.toml b/Cargo.toml index ba99e45..3b57759 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ default = [] [dependencies] bs58 = "0.5.0" bytemuck = "1.14.3" +mpl-token-metadata = "4.1.2" num_enum = "0.7.2" shank = "0.3.0" solana-program = "^1.16" diff --git a/src/consts.rs b/src/consts.rs index b6227dd..3c693e3 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -3,7 +3,7 @@ use solana_program::{keccak::Hash, pubkey, pubkey::Pubkey}; /// The unix timestamp after which mining is allowed. pub const START_AT: i64 = 0; -/// Bus pubkeys +/// The addresses of the bus accounts. pub const BUS_ADDRESSES: [Pubkey; BUS_COUNT] = [ pubkey!("85JC7qU7pkjYdvvXewfzgjCBZvugtrnPKYE9mzPD2ajJ"), pubkey!("FXCPt8PPwNQF8NVFDvdnHRENpkWexGMr5t8EnSoBsbns"), @@ -15,10 +15,13 @@ pub const BUS_ADDRESSES: [Pubkey; BUS_COUNT] = [ pubkey!("72GSzz967ePb6mDrZYzmwyFFrfNUgH2PUwwocfeyjxLB"), ]; -/// The mint address of the ORE token. +/// The address of the Ore mint metadata account. +pub const METADATA_ADDRESS: Pubkey = pubkey!("4nbf4yufkBjJbZjZrz5D6L4nRyRbiw9rvKLTesCVpqnB"); + +/// The address of the Ore mint account. pub const MINT_ADDRESS: Pubkey = pubkey!("tmResQt9qPVRhAh74fMxginQqHBG74Ls3Nou1rkvCg7"); -/// Treasury address +/// The address of the treasury account. pub const TREASURY_ADDRESS: Pubkey = pubkey!("nLCGcWmqqLC2UVBb3neVQWhzzJd8GAJshvasczmVm94"); /// The initial reward rate to payout in the first epoch. @@ -66,6 +69,9 @@ static_assertions::const_assert!( /// The seed of the bus account PDA. pub const BUS: &[u8] = b"bus"; +/// The seed of the mint account PDA. +pub const METADATA: &[u8] = b"metadata"; + /// The seed of the mint account PDA. pub const MINT: &[u8] = b"mint"; @@ -74,3 +80,12 @@ pub const PROOF: &[u8] = b"proof"; /// The seed of the treasury account PDA. pub const TREASURY: &[u8] = b"treasury"; + +/// The name for token metadata. +pub const METADATA_NAME: &str = "Ore"; + +/// The ticker symbol for token metadata. +pub const METADATA_SYMBOL: &str = "ORE"; + +/// The uri for token metdata. +pub const METADATA_URI: &str = "https://ore.supply/public/metadata.json"; diff --git a/src/instruction.rs b/src/instruction.rs index 36fe639..c5cc038 100644 --- a/src/instruction.rs +++ b/src/instruction.rs @@ -8,8 +8,8 @@ use solana_program::{ }; use crate::{ - impl_instruction_from_bytes, impl_to_bytes, state::Hash, BUS, MINT, MINT_ADDRESS, PROOF, - TREASURY, TREASURY_ADDRESS, + impl_instruction_from_bytes, impl_to_bytes, state::Hash, BUS, METADATA, MINT, MINT_ADDRESS, + PROOF, TREASURY, TREASURY_ADDRESS, }; #[repr(u8)] @@ -66,13 +66,15 @@ pub enum OreInstruction { #[account(7, name = "bus_5", desc = "Ore bus account 5", writable)] #[account(8, name = "bus_6", desc = "Ore bus account 6", writable)] #[account(9, name = "bus_7", desc = "Ore bus account 7", writable)] - #[account(10, name = "mint", desc = "Ore token mint account")] - #[account(11, name = "treasury", desc = "Ore treasury account")] - #[account(12, name = "treasury_tokens", desc = "Ore treasury token account", writable)] - #[account(13, name = "system_program", desc = "Solana system program")] - #[account(14, name = "token_program", desc = "SPL token program")] - #[account(15, name = "associated_token_program", desc = "SPL associated token program")] - #[account(16, name = "rent", desc = "Solana rent sysvar")] + #[account(10, name = "metadata", desc = "Ore mint metadata account", writable)] + #[account(11, name = "mint", desc = "Ore mint account", writable)] + #[account(12, name = "treasury", desc = "Ore treasury account", writable)] + #[account(13, name = "treasury_tokens", desc = "Ore treasury token account", writable)] + #[account(14, name = "system_program", desc = "Solana system program")] + #[account(15, name = "token_program", desc = "SPL token program")] + #[account(16, name = "associated_token_program", desc = "SPL associated token program")] + #[account(17, name = "mpl_token_metadata", desc = "MPL token metadata program")] + #[account(18, name = "rent", desc = "Solana rent sysvar")] Initialize = 100, #[account(0, name = "ore_program", desc = "Ore program")] @@ -101,6 +103,7 @@ pub struct InitializeArgs { pub bus_5_bump: u8, pub bus_6_bump: u8, pub bus_7_bump: u8, + pub metadata_bump: u8, pub mint_bump: u8, pub treasury_bump: u8, } @@ -168,6 +171,14 @@ pub fn initialize(signer: Pubkey) -> Instruction { ]; let mint_pda = Pubkey::find_program_address(&[MINT], &crate::id()); let treasury_pda = Pubkey::find_program_address(&[TREASURY], &crate::id()); + let metadata_pda = Pubkey::find_program_address( + &[ + METADATA, + mpl_token_metadata::ID.as_ref(), + mint_pda.0.as_ref(), + ], + &mpl_token_metadata::ID, + ); Instruction { program_id: crate::id(), accounts: vec![ @@ -180,12 +191,14 @@ pub fn initialize(signer: Pubkey) -> Instruction { AccountMeta::new(bus_pdas[5].0, false), AccountMeta::new(bus_pdas[6].0, false), AccountMeta::new(bus_pdas[7].0, false), + AccountMeta::new(metadata_pda.0, false), AccountMeta::new(mint_pda.0, false), AccountMeta::new(treasury_pda.0, false), AccountMeta::new(treasury_tokens, false), AccountMeta::new_readonly(system_program::id(), false), AccountMeta::new_readonly(spl_token::id(), false), AccountMeta::new_readonly(spl_associated_token_account::id(), false), + AccountMeta::new_readonly(mpl_token_metadata::ID, false), AccountMeta::new_readonly(sysvar::rent::id(), false), ], data: [ @@ -199,6 +212,7 @@ pub fn initialize(signer: Pubkey) -> Instruction { bus_5_bump: bus_pdas[5].1, bus_6_bump: bus_pdas[6].1, bus_7_bump: bus_pdas[7].1, + metadata_bump: metadata_pda.1, mint_bump: mint_pda.1, treasury_bump: treasury_pda.1, } diff --git a/src/loaders.rs b/src/loaders.rs index b9a00af..40fbeb6 100644 --- a/src/loaders.rs +++ b/src/loaders.rs @@ -201,8 +201,9 @@ pub fn load_token_account<'a, 'info>( pub fn load_uninitialized_pda<'a, 'info>( info: &'a AccountInfo<'info>, seeds: &[&[u8]], + program_id: &Pubkey, ) -> Result<(), ProgramError> { - let key = Pubkey::create_program_address(seeds, &crate::id())?; + let key = Pubkey::create_program_address(seeds, program_id)?; if info.key.ne(&key) { return Err(ProgramError::InvalidSeeds); } diff --git a/src/processor/initialize.rs b/src/processor/initialize.rs index 1b9a25b..991b42c 100644 --- a/src/processor/initialize.rs +++ b/src/processor/initialize.rs @@ -17,7 +17,8 @@ use crate::{ utils::create_pda, utils::AccountDeserialize, utils::Discriminator, - BUS, BUS_ADDRESSES, BUS_COUNT, INITIAL_DIFFICULTY, INITIAL_REWARD_RATE, MINT, MINT_ADDRESS, + BUS, BUS_ADDRESSES, BUS_COUNT, INITIAL_DIFFICULTY, INITIAL_REWARD_RATE, METADATA, + METADATA_ADDRESS, METADATA_NAME, METADATA_SYMBOL, METADATA_URI, MINT, MINT_ADDRESS, TOKEN_DECIMALS, TREASURY, TREASURY_ADDRESS, }; @@ -45,29 +46,47 @@ pub fn process_initialize<'a, 'info>( let args = InitializeArgs::try_from_bytes(data)?; // Load accounts - let [signer, bus_0_info, bus_1_info, bus_2_info, bus_3_info, bus_4_info, bus_5_info, bus_6_info, bus_7_info, mint_info, treasury_info, treasury_tokens_info, system_program, token_program, associated_token_program, rent_sysvar] = + let [signer, bus_0_info, bus_1_info, bus_2_info, bus_3_info, bus_4_info, bus_5_info, bus_6_info, bus_7_info, metadata_info, mint_info, treasury_info, treasury_tokens_info, system_program, token_program, associated_token_program, metadata_program, rent_sysvar] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; load_signer(signer)?; - load_uninitialized_pda(bus_0_info, &[BUS, &[0], &[args.bus_0_bump]])?; - load_uninitialized_pda(bus_1_info, &[BUS, &[1], &[args.bus_1_bump]])?; - load_uninitialized_pda(bus_2_info, &[BUS, &[2], &[args.bus_2_bump]])?; - load_uninitialized_pda(bus_3_info, &[BUS, &[3], &[args.bus_3_bump]])?; - load_uninitialized_pda(bus_4_info, &[BUS, &[4], &[args.bus_4_bump]])?; - load_uninitialized_pda(bus_5_info, &[BUS, &[5], &[args.bus_5_bump]])?; - load_uninitialized_pda(bus_6_info, &[BUS, &[6], &[args.bus_6_bump]])?; - load_uninitialized_pda(bus_7_info, &[BUS, &[7], &[args.bus_7_bump]])?; - load_uninitialized_pda(mint_info, &[MINT, &[args.mint_bump]])?; - load_uninitialized_pda(treasury_info, &[TREASURY, &[args.treasury_bump]])?; + load_uninitialized_pda(bus_0_info, &[BUS, &[0], &[args.bus_0_bump]], &crate::id())?; + load_uninitialized_pda(bus_1_info, &[BUS, &[1], &[args.bus_1_bump]], &crate::id())?; + load_uninitialized_pda(bus_2_info, &[BUS, &[2], &[args.bus_2_bump]], &crate::id())?; + load_uninitialized_pda(bus_3_info, &[BUS, &[3], &[args.bus_3_bump]], &crate::id())?; + load_uninitialized_pda(bus_4_info, &[BUS, &[4], &[args.bus_4_bump]], &crate::id())?; + load_uninitialized_pda(bus_5_info, &[BUS, &[5], &[args.bus_5_bump]], &crate::id())?; + load_uninitialized_pda(bus_6_info, &[BUS, &[6], &[args.bus_6_bump]], &crate::id())?; + load_uninitialized_pda(bus_7_info, &[BUS, &[7], &[args.bus_7_bump]], &crate::id())?; + load_uninitialized_pda( + metadata_info, + &[ + METADATA, + mpl_token_metadata::ID.as_ref(), + MINT_ADDRESS.as_ref(), + &[args.metadata_bump], + ], + &mpl_token_metadata::ID, + )?; + load_uninitialized_pda(mint_info, &[MINT, &[args.mint_bump]], &crate::id())?; + load_uninitialized_pda( + treasury_info, + &[TREASURY, &[args.treasury_bump]], + &crate::id(), + )?; load_uninitialized_account(treasury_tokens_info)?; load_program(system_program, system_program::id())?; load_program(token_program, spl_token::id())?; load_program(associated_token_program, spl_associated_token_account::id())?; + load_program(metadata_program, mpl_token_metadata::ID)?; load_sysvar(rent_sysvar, sysvar::rent::id())?; // Verify keys + if metadata_info.key.ne(&METADATA_ADDRESS) { + return Err(ProgramError::InvalidSeeds); + } if mint_info.key.ne(&MINT_ADDRESS) { return Err(ProgramError::InvalidSeeds); } @@ -155,6 +174,32 @@ pub fn process_initialize<'a, 'info>( &[&[MINT, &[args.mint_bump]]], )?; + // Initialize mint metadata + mpl_token_metadata::instructions::CreateMetadataAccountV3Cpi { + __program: metadata_program, + metadata: metadata_info, + mint: mint_info, + mint_authority: treasury_info, + payer: signer, + update_authority: (signer, true), + system_program, + rent: Some(rent_sysvar), + __args: mpl_token_metadata::instructions::CreateMetadataAccountV3InstructionArgs { + data: mpl_token_metadata::types::DataV2 { + name: METADATA_NAME.to_string(), + symbol: METADATA_SYMBOL.to_string(), + uri: METADATA_URI.to_string(), + seller_fee_basis_points: 0, + creators: None, + collection: None, + uses: None, + }, + is_mutable: true, + collection_details: None, + }, + } + .invoke_signed(&[&[TREASURY, &[args.treasury_bump]]])?; + // Initialize treasury token account solana_program::program::invoke( &spl_associated_token_account::instruction::create_associated_token_account( diff --git a/src/processor/register.rs b/src/processor/register.rs index 97418d3..0e44f23 100644 --- a/src/processor/register.rs +++ b/src/processor/register.rs @@ -36,7 +36,11 @@ pub fn process_register<'a, 'info>( return Err(ProgramError::NotEnoughAccountKeys); }; load_signer(signer)?; - load_uninitialized_pda(proof_info, &[PROOF, signer.key.as_ref(), &[args.bump]])?; + load_uninitialized_pda( + proof_info, + &[PROOF, signer.key.as_ref(), &[args.bump]], + &crate::id(), + )?; load_program(system_program, system_program::id())?; // Initialize proof diff --git a/tests/buffers/metadata_program.bpf b/tests/buffers/metadata_program.bpf new file mode 100644 index 0000000..3ebd1b6 Binary files /dev/null and b/tests/buffers/metadata_program.bpf differ diff --git a/tests/test_initialize.rs b/tests/test_initialize.rs index 1dba2b6..6817ee7 100644 --- a/tests/test_initialize.rs +++ b/tests/test_initialize.rs @@ -1,11 +1,19 @@ +use mpl_token_metadata::{ + accounts::Metadata, + types::{Key, TokenStandard}, +}; use ore::{ state::{Bus, Treasury}, utils::AccountDeserialize, - BUS_ADDRESSES, BUS_COUNT, INITIAL_DIFFICULTY, INITIAL_REWARD_RATE, MINT_ADDRESS, TREASURY, + BUS_ADDRESSES, BUS_COUNT, INITIAL_DIFFICULTY, INITIAL_REWARD_RATE, METADATA_ADDRESS, + METADATA_NAME, METADATA_SYMBOL, METADATA_URI, MINT_ADDRESS, TREASURY, }; -use solana_program::{hash::Hash, program_option::COption, program_pack::Pack, pubkey::Pubkey}; -use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_program::{ + hash::Hash, program_option::COption, program_pack::Pack, pubkey::Pubkey, rent::Rent, +}; +use solana_program_test::{processor, read_file, BanksClient, ProgramTest}; use solana_sdk::{ + account::Account, signature::{Keypair, Signer}, transaction::Transaction, }; @@ -57,6 +65,27 @@ async fn test_initialize() { assert_eq!(mint.is_initialized, true); assert_eq!(mint.freeze_authority, COption::None); + // Test metadata state + let metadata_account = banks.get_account(METADATA_ADDRESS).await.unwrap().unwrap(); + assert_eq!(metadata_account.owner, mpl_token_metadata::ID); + let metadata = Metadata::from_bytes(&metadata_account.data).unwrap(); + assert_eq!(metadata.key, Key::MetadataV1); + assert_eq!(metadata.update_authority, payer.pubkey()); + assert_eq!(metadata.mint, MINT_ADDRESS); + assert_eq!(metadata.name.trim_end_matches('\0'), METADATA_NAME); + assert_eq!(metadata.symbol.trim_end_matches('\0'), METADATA_SYMBOL); + assert_eq!(metadata.uri.trim_end_matches('\0'), METADATA_URI); + assert_eq!(metadata.seller_fee_basis_points, 0); + assert_eq!(metadata.creators, None); + assert_eq!(metadata.primary_sale_happened, false); + assert_eq!(metadata.is_mutable, true); + assert_eq!(metadata.edition_nonce, Some(u8::MAX)); + assert_eq!(metadata.token_standard, Some(TokenStandard::Fungible)); + assert_eq!(metadata.collection, None); + assert_eq!(metadata.uses, None); + assert_eq!(metadata.collection_details, None); + assert_eq!(metadata.programmable_config, None); + // Test treasury token state let treasury_tokens_account = banks .get_account(treasury_tokens_address) @@ -78,5 +107,19 @@ async fn test_initialize() { async fn setup_program_test_env() -> (BanksClient, Keypair, Hash) { let mut program_test = ProgramTest::new("ore", ore::ID, processor!(ore::process_instruction)); program_test.prefer_bpf(true); + + // Setup metadata program + let data = read_file(&"tests/buffers/metadata_program.bpf"); + program_test.add_account( + mpl_token_metadata::ID, + Account { + lamports: Rent::default().minimum_balance(data.len()).max(1), + data, + owner: solana_sdk::bpf_loader::id(), + executable: true, + rent_epoch: 0, + }, + ); + program_test.start().await } diff --git a/tests/test_mine.rs b/tests/test_mine.rs index f38785b..f46bad5 100644 --- a/tests/test_mine.rs +++ b/tests/test_mine.rs @@ -1,7 +1,7 @@ use std::{mem::size_of, str::FromStr}; use ore::{ - instruction::{MineArgs, OreInstruction, RegisterArgs}, + instruction::{MineArgs, OreInstruction}, state::{Bus, Proof, Treasury}, utils::{AccountDeserialize, Discriminator}, BUS_ADDRESSES, BUS_COUNT, INITIAL_REWARD_RATE, MINT_ADDRESS, PROOF, TOKEN_DECIMALS, TREASURY,