diff --git a/src/instruction.rs b/src/instruction.rs index c46ab97..df1387b 100644 --- a/src/instruction.rs +++ b/src/instruction.rs @@ -62,9 +62,9 @@ pub enum OreInstruction { #[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 = "rent", desc = "Solana rent sysvar")] + #[account(15, name = "associated_token_program", desc = "SPL associated token program")] + #[account(16, name = "rent", desc = "Solana rent sysvar")] Initialize = 100, - // #[account(15, name = "associated_token_program", desc = "SPL associated token program")] // TODO // #[account(0, name = "ore_program", desc = "Ore program")] diff --git a/src/lib.rs b/src/lib.rs index 7f73a30..c7519d8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,30 +1,19 @@ pub mod instruction; mod loaders; +mod processor; +pub mod state; +mod utils; -use std::mem::size_of; - -use bytemuck::{Pod, Zeroable}; -use solana_program::program_pack::Pack; -use solana_program::{self, sysvar}; +use processor::*; use solana_program::{ - account_info::{next_account_info, AccountInfo}, - declare_id, - entrypoint::ProgramResult, - keccak::{hashv, Hash}, - program_error::ProgramError, - pubkey, - pubkey::Pubkey, - rent::Rent, - system_program, - sysvar::Sysvar, + self, account_info::AccountInfo, declare_id, entrypoint::ProgramResult, keccak::Hash, + program_error::ProgramError, pubkey, pubkey::Pubkey, }; use instruction::*; -use loaders::*; -use spl_token::state::Mint; // TODO Test admin and difficulty adjustment functions -// TODO Use more decimals? +// TODO Increase decimals? declare_id!("CeJShZEAzBLwtcLQvbZc7UT38e4nUTn63Za5UFyYYDTS"); @@ -45,7 +34,8 @@ pub const INITIAL_DIFFICULTY: Hash = Hash::new_from_array([ ]); /// The mint address of the ORE token. -pub const MINT_ADDRESS: Pubkey = pubkey!("37TDfMS8NHpyhyCXBrY9m7rRrtj1f7TrFzD1iXqmTeUX"); +// pub const MINT_ADDRESS: Pubkey = pubkey!("37TDfMS8NHpyhyCXBrY9m7rRrtj1f7TrFzD1iXqmTeUX"); +pub const MINT_ADDRESS: Pubkey = pubkey!("DY4JVebraRXg9BGt4MRU4mvqHGDzmi2Ay1HGjDU5YeNf"); /// The decimal precision of the ORE token. /// Using SI prefixes, the smallest indivisible unit of ORE is a nanoORE. @@ -80,285 +70,6 @@ static_assertions::const_assert!( (MAX_EPOCH_REWARDS / BUS_COUNT as u64) * BUS_COUNT as u64 == MAX_EPOCH_REWARDS ); -pub fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - data: &[u8], -) -> ProgramResult { - let (tag, data) = data - .split_first() - .ok_or(ProgramError::InvalidInstructionData)?; - - let ix = OreInstruction::try_from(*tag).or(Err(ProgramError::InvalidInstructionData))?; - - match ix { - OreInstruction::Epoch => process_epoch(program_id, accounts, data)?, - OreInstruction::Proof => process_proof(program_id, accounts, data)?, - OreInstruction::Mine => process_mine(program_id, accounts, data)?, - OreInstruction::Claim => process_claim(program_id, accounts, data)?, - OreInstruction::Initialize => process_initialize(program_id, accounts, data)?, - } - - Ok(()) -} - -fn process_epoch<'a, 'info>( - _program_id: &Pubkey, - accounts: &'a [AccountInfo<'info>], - data: &[u8], -) -> ProgramResult { - // TODO - Ok(()) -} - -fn process_proof<'a, 'info>( - _program_id: &Pubkey, - accounts: &'a [AccountInfo<'info>], - data: &[u8], -) -> ProgramResult { - // TODO - Ok(()) -} - -fn process_mine<'a, 'info>( - _program_id: &Pubkey, - accounts: &'a [AccountInfo<'info>], - data: &[u8], -) -> ProgramResult { - // TODO - Ok(()) -} - -fn process_claim<'a, 'info>( - _program_id: &Pubkey, - accounts: &'a [AccountInfo<'info>], - data: &[u8], -) -> ProgramResult { - // TODO - Ok(()) -} - -fn process_initialize<'a, 'info>( - _program_id: &Pubkey, - accounts: &'a [AccountInfo<'info>], - data: &[u8], -) -> ProgramResult { - let accounts_iter = &mut accounts.iter(); - let args = bytemuck::try_from_bytes::(data) - .or(Err(ProgramError::InvalidInstructionData))?; - - // Account 1: Signer - let signer = load_signer(next_account_info(accounts_iter)?)?; - - // Accounts 2-9: Busses - #[rustfmt::skip] - let busses = vec![ - load_uninitialized_pda(next_account_info(accounts_iter)?, &[BUS, &[0], &[args.bus_0_bump]])?, - load_uninitialized_pda(next_account_info(accounts_iter)?, &[BUS, &[1], &[args.bus_1_bump]])?, - load_uninitialized_pda(next_account_info(accounts_iter)?, &[BUS, &[2], &[args.bus_2_bump]])?, - load_uninitialized_pda(next_account_info(accounts_iter)?, &[BUS, &[3], &[args.bus_3_bump]])?, - load_uninitialized_pda(next_account_info(accounts_iter)?, &[BUS, &[4], &[args.bus_4_bump]])?, - load_uninitialized_pda(next_account_info(accounts_iter)?, &[BUS, &[5], &[args.bus_5_bump]])?, - load_uninitialized_pda(next_account_info(accounts_iter)?, &[BUS, &[6], &[args.bus_6_bump]])?, - load_uninitialized_pda(next_account_info(accounts_iter)?, &[BUS, &[7], &[args.bus_7_bump]])?, - ]; - - // Account 10: Mint - #[rustfmt::skip] - let mint = load_uninitialized_pda(next_account_info(accounts_iter)?, &[MINT, &[args.mint_bump]])?; - - // Account 11: Treasury - #[rustfmt::skip] - let treasury_account_info = load_uninitialized_pda(next_account_info(accounts_iter)?, &[TREASURY, &[args.treasury_bump]])?; - - // Account 12: Treasury tokens - let treasury_tokens = load_uninitialized_account(next_account_info(accounts_iter)?)?; - - // Account 13: System program - let system_program = load_account(next_account_info(accounts_iter)?, system_program::id())?; - - // Account 14: Token program - let token_program = load_account(next_account_info(accounts_iter)?, spl_token::id())?; - - // Account 15: Rent sysvar - let rent_sysvar = load_account(next_account_info(accounts_iter)?, sysvar::rent::id())?; - - // Initialize bus accounts - let bus_bumps = vec![ - args.bus_0_bump, - args.bus_1_bump, - args.bus_2_bump, - args.bus_3_bump, - args.bus_4_bump, - args.bus_5_bump, - args.bus_6_bump, - args.bus_7_bump, - ]; - for i in 0..BUS_COUNT { - create_pda( - busses[i], - &crate::id(), - size_of::(), - &[BUS, &[i as u8], &[bus_bumps[i]]], - system_program, - signer, - )?; - busses[i].try_borrow_mut_data()?.copy_from_slice( - Bus { - bump: bus_bumps[i] as u32, - id: i as u32, - available_rewards: 0, - } - .to_bytes(), - ); - } - - // Initialize treasury - create_pda( - treasury_account_info, - &crate::id(), - size_of::(), - &[TREASURY, &[args.treasury_bump]], - system_program, - signer, - )?; - let mut treasury_data = treasury_account_info.data.borrow_mut(); - let mut treasury = bytemuck::try_from_bytes_mut::(&mut treasury_data).unwrap(); - treasury.bump = args.treasury_bump as u64; - treasury.admin = *signer.key; - treasury.epoch_start_at = 0; - treasury.reward_rate = INITIAL_REWARD_RATE; - treasury.total_claimed_rewards = 0; - - // Initialize mint - create_pda( - mint, - &spl_token::id(), - Mint::LEN, - &[MINT, &[args.mint_bump]], - system_program, - signer, - )?; - solana_program::program::invoke_signed( - &spl_token::instruction::initialize_mint( - &spl_token::id(), - mint.key, - treasury_account_info.key, - None, - TOKEN_DECIMALS, - )?, - &[ - token_program.clone(), - mint.clone(), - treasury_account_info.clone(), - rent_sysvar.clone(), - ], - &[&[MINT, &[args.mint_bump]]], - )?; - - // TODO Initialize treasury token account - create_pda( - mint, - &spl_token::id(), - spl_token::state::Account::LEN, - &[MINT, &[args.mint_bump]], - system_program, - signer, - )?; - solana_program::program::invoke_signed( - &spl_token::instruction::initialize_mint( - &spl_token::id(), - mint.key, - treasury_account_info.key, - None, - TOKEN_DECIMALS, - )?, - &[ - token_program.clone(), - mint.clone(), - treasury_account_info.clone(), - rent_sysvar.clone(), - ], - &[&[MINT, &[args.mint_bump]]], - )?; - - Ok(()) -} - -fn validate_hash( - current_hash: Hash, - hash: Hash, - signer: Pubkey, - nonce: u64, - difficulty: Hash, -) -> Result<(), ProgramError> { - // Validate hash correctness. - let hash_ = hashv(&[ - current_hash.as_ref(), - signer.as_ref(), - nonce.to_be_bytes().as_slice(), - ]); - if !hash.eq(&hash_) { - return Err(ProgramError::Custom(1)); - } - - // Validate hash difficulty. - if !hash.le(&difficulty) { - return Err(ProgramError::Custom(1)); - } - - Ok(()) -} - -fn calculate_new_reward_rate(current_rate: u64, epoch_rewards: u64) -> u64 { - // Avoid division by zero. Leave the reward rate unchanged. - if epoch_rewards.eq(&0) { - return current_rate; - } - - // Calculate new reward rate. - let new_rate = (current_rate as u128) - .saturating_mul(TARGET_EPOCH_REWARDS as u128) - .saturating_div(epoch_rewards as u128) as u64; - - // Smooth reward rate to not change by more than a constant factor from one epoch to the next. - let new_rate_min = current_rate.saturating_div(SMOOTHING_FACTOR); - let new_rate_max = current_rate.saturating_mul(SMOOTHING_FACTOR); - let new_rate_smoothed = new_rate_min.max(new_rate_max.min(new_rate)); - - // Prevent reward rate from dropping below 1 or exceeding BUS_EPOCH_REWARDS and return. - new_rate_smoothed.max(1).min(BUS_EPOCH_REWARDS) -} - -/// Creates a new pda -#[inline(always)] -pub fn create_pda<'a, 'info>( - target_account: &'a AccountInfo<'info>, - owner: &Pubkey, - space: usize, - pda_seeds: &[&[u8]], - system_program: &'a AccountInfo<'info>, - payer: &'a AccountInfo<'info>, -) -> ProgramResult { - let rent = Rent::get()?; - solana_program::program::invoke_signed( - &solana_program::system_instruction::create_account( - payer.key, - target_account.key, - rent.minimum_balance(space as usize), - space as u64, - owner, - ), - &[ - payer.clone(), - target_account.clone(), - system_program.clone(), - ], - &[pda_seeds], - )?; - Ok(()) -} - /// The seed of the bus account PDA. pub const BUS: &[u8] = b"bus"; @@ -371,144 +82,24 @@ pub const PROOF: &[u8] = b"proof"; /// The seed of the treasury account PDA. pub const TREASURY: &[u8] = b"treasury"; -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -pub struct Bus { - /// The bump of the bus account PDA. - pub bump: u32, +/// Processes the incoming instruction +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let (tag, data) = data + .split_first() + .ok_or(ProgramError::InvalidInstructionData)?; - /// The ID of the bus account. - pub id: u32, + let ix = OreInstruction::try_from(*tag).or(Err(ProgramError::InvalidInstructionData))?; + match ix { + OreInstruction::Epoch => process_epoch(program_id, accounts, data)?, + OreInstruction::Proof => process_proof(program_id, accounts, data)?, + OreInstruction::Mine => process_mine(program_id, accounts, data)?, + OreInstruction::Claim => process_claim(program_id, accounts, data)?, + OreInstruction::Initialize => process_initialize(program_id, accounts, data)?, + } - /// The quantity of rewards this bus can issue in the current epoch epoch. - pub available_rewards: u64, -} - -impl Bus { - pub fn to_bytes(&self) -> &[u8] { - bytemuck::bytes_of(self) - } -} - -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -pub struct Treasury { - /// The bump of the treasury account PDA. - pub bump: u64, - - /// The admin authority with permission to update the difficulty. - pub admin: Pubkey, - - /// The hash difficulty. - // pub difficulty: Hash, - - /// The timestamp of the start of the current epoch. - pub epoch_start_at: i64, - - /// The reward rate to payout to miners for submiting valid hashes. - pub reward_rate: u64, - - /// The total lifetime claimed rewards. - pub total_claimed_rewards: u64, -} - -impl Treasury { - pub fn to_bytes(&self) -> &[u8] { - bytemuck::bytes_of(self) - } -} - -#[cfg(test)] -mod tests { - use solana_program::{ - keccak::{hashv, Hash}, - pubkey::Pubkey, - }; - - use crate::{calculate_new_reward_rate, validate_hash, SMOOTHING_FACTOR, TARGET_EPOCH_REWARDS}; - - #[test] - fn test_validate_hash_pass() { - let h1 = Hash::new_from_array([1; 32]); - let signer = Pubkey::new_unique(); - let nonce = 10u64; - let difficulty = Hash::new_from_array([255; 32]); - let h2 = hashv(&[ - h1.to_bytes().as_slice(), - signer.to_bytes().as_slice(), - nonce.to_be_bytes().as_slice(), - ]); - let res = validate_hash(h1, h2, signer, nonce, difficulty); - assert!(res.is_ok()); - } - - #[test] - fn test_validate_hash_fail() { - let h1 = Hash::new_from_array([1; 32]); - let signer = Pubkey::new_unique(); - let nonce = 10u64; - let difficulty = Hash::new_from_array([255; 32]); - let h2 = Hash::new_from_array([2; 32]); - let res = validate_hash(h1, h2, signer, nonce, difficulty); - assert!(res.is_err()); - } - - #[test] - fn test_validate_hash_fail_difficulty() { - let h1 = Hash::new_from_array([1; 32]); - let signer = Pubkey::new_unique(); - let nonce = 10u64; - let difficulty = Hash::new_from_array([0; 32]); - let h2 = hashv(&[ - h1.to_bytes().as_slice(), - signer.to_bytes().as_slice(), - nonce.to_be_bytes().as_slice(), - ]); - let res = validate_hash(h1, h2, signer, nonce, difficulty); - assert!(res.is_err()); - } - - #[test] - fn test_calculate_new_reward_rate_stable() { - let current_rate = 1000; - let new_rate = calculate_new_reward_rate(current_rate, TARGET_EPOCH_REWARDS); - assert!(new_rate.eq(¤t_rate)); - } - - #[test] - fn test_calculate_new_reward_rate_no_chage() { - let current_rate = 1000; - let new_rate = calculate_new_reward_rate(current_rate, 0); - assert!(new_rate.eq(¤t_rate)); - } - - #[test] - fn test_calculate_new_reward_rate_lower() { - let current_rate = 1000; - let new_rate = - calculate_new_reward_rate(current_rate, TARGET_EPOCH_REWARDS.saturating_add(1_000_000)); - assert!(new_rate.lt(¤t_rate)); - } - - #[test] - fn test_calculate_new_reward_rate_higher() { - let current_rate = 1000; - let new_rate = - calculate_new_reward_rate(current_rate, TARGET_EPOCH_REWARDS.saturating_sub(1_000_000)); - assert!(new_rate.gt(¤t_rate)); - } - - #[test] - fn test_calculate_new_reward_rate_max_smooth() { - let current_rate = 1000; - let new_rate = calculate_new_reward_rate(current_rate, 1); - assert!(new_rate.eq(¤t_rate.saturating_mul(SMOOTHING_FACTOR))); - } - - #[test] - fn test_calculate_new_reward_rate_min_smooth() { - let current_rate = 1000; - let new_rate = calculate_new_reward_rate(current_rate, u64::MAX); - assert!(new_rate.eq(¤t_rate.saturating_div(SMOOTHING_FACTOR))); - } + Ok(()) } diff --git a/src/processor/claim.rs b/src/processor/claim.rs new file mode 100644 index 0000000..cca32ae --- /dev/null +++ b/src/processor/claim.rs @@ -0,0 +1,10 @@ +use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; + +pub fn process_claim<'a, 'info>( + _program_id: &Pubkey, + accounts: &'a [AccountInfo<'info>], + data: &[u8], +) -> ProgramResult { + // TODO + Ok(()) +} diff --git a/src/processor/epoch.rs b/src/processor/epoch.rs new file mode 100644 index 0000000..5b56bc7 --- /dev/null +++ b/src/processor/epoch.rs @@ -0,0 +1,81 @@ +use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; + +use crate::{BUS_EPOCH_REWARDS, SMOOTHING_FACTOR, TARGET_EPOCH_REWARDS}; + +pub fn process_epoch<'a, 'info>( + _program_id: &Pubkey, + accounts: &'a [AccountInfo<'info>], + data: &[u8], +) -> ProgramResult { + // TODO + Ok(()) +} + +pub(crate) fn calculate_new_reward_rate(current_rate: u64, epoch_rewards: u64) -> u64 { + // Avoid division by zero. Leave the reward rate unchanged. + if epoch_rewards.eq(&0) { + return current_rate; + } + + // Calculate new reward rate. + let new_rate = (current_rate as u128) + .saturating_mul(TARGET_EPOCH_REWARDS as u128) + .saturating_div(epoch_rewards as u128) as u64; + + // Smooth reward rate to not change by more than a constant factor from one epoch to the next. + let new_rate_min = current_rate.saturating_div(SMOOTHING_FACTOR); + let new_rate_max = current_rate.saturating_mul(SMOOTHING_FACTOR); + let new_rate_smoothed = new_rate_min.max(new_rate_max.min(new_rate)); + + // Prevent reward rate from dropping below 1 or exceeding BUS_EPOCH_REWARDS and return. + new_rate_smoothed.max(1).min(BUS_EPOCH_REWARDS) +} + +#[cfg(test)] +mod tests { + use crate::{calculate_new_reward_rate, SMOOTHING_FACTOR, TARGET_EPOCH_REWARDS}; + + #[test] + fn test_calculate_new_reward_rate_stable() { + let current_rate = 1000; + let new_rate = calculate_new_reward_rate(current_rate, TARGET_EPOCH_REWARDS); + assert!(new_rate.eq(¤t_rate)); + } + + #[test] + fn test_calculate_new_reward_rate_no_chage() { + let current_rate = 1000; + let new_rate = calculate_new_reward_rate(current_rate, 0); + assert!(new_rate.eq(¤t_rate)); + } + + #[test] + fn test_calculate_new_reward_rate_lower() { + let current_rate = 1000; + let new_rate = + calculate_new_reward_rate(current_rate, TARGET_EPOCH_REWARDS.saturating_add(1_000_000)); + assert!(new_rate.lt(¤t_rate)); + } + + #[test] + fn test_calculate_new_reward_rate_higher() { + let current_rate = 1000; + let new_rate = + calculate_new_reward_rate(current_rate, TARGET_EPOCH_REWARDS.saturating_sub(1_000_000)); + assert!(new_rate.gt(¤t_rate)); + } + + #[test] + fn test_calculate_new_reward_rate_max_smooth() { + let current_rate = 1000; + let new_rate = calculate_new_reward_rate(current_rate, 1); + assert!(new_rate.eq(¤t_rate.saturating_mul(SMOOTHING_FACTOR))); + } + + #[test] + fn test_calculate_new_reward_rate_min_smooth() { + let current_rate = 1000; + let new_rate = calculate_new_reward_rate(current_rate, u64::MAX); + assert!(new_rate.eq(¤t_rate.saturating_div(SMOOTHING_FACTOR))); + } +} diff --git a/src/processor/initialize.rs b/src/processor/initialize.rs new file mode 100644 index 0000000..5c998ab --- /dev/null +++ b/src/processor/initialize.rs @@ -0,0 +1,198 @@ +use std::mem::size_of; + +use solana_program::program_pack::Pack; +use solana_program::{self, sysvar}; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + program_error::ProgramError, + pubkey::Pubkey, + system_program, +}; +use spl_token::state::Mint; + +use crate::{instruction::*, BUS, INITIAL_DIFFICULTY, MINT_ADDRESS}; +use crate::{ + loaders::*, + state::{Bus, Treasury}, + utils::create_pda, + BUS_COUNT, INITIAL_REWARD_RATE, MINT, TOKEN_DECIMALS, TREASURY, +}; + +pub fn process_initialize<'a, 'info>( + _program_id: &Pubkey, + accounts: &'a [AccountInfo<'info>], + data: &[u8], +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + let args = bytemuck::try_from_bytes::(data) + .or(Err(ProgramError::InvalidInstructionData))?; + + // Account 1: Signer + let signer = load_signer(next_account_info(accounts_iter)?)?; + + // Accounts 2-9: Busses + let busses = vec![ + load_uninitialized_pda( + next_account_info(accounts_iter)?, + &[BUS, &[0], &[args.bus_0_bump]], + )?, + load_uninitialized_pda( + next_account_info(accounts_iter)?, + &[BUS, &[1], &[args.bus_1_bump]], + )?, + load_uninitialized_pda( + next_account_info(accounts_iter)?, + &[BUS, &[2], &[args.bus_2_bump]], + )?, + load_uninitialized_pda( + next_account_info(accounts_iter)?, + &[BUS, &[3], &[args.bus_3_bump]], + )?, + load_uninitialized_pda( + next_account_info(accounts_iter)?, + &[BUS, &[4], &[args.bus_4_bump]], + )?, + load_uninitialized_pda( + next_account_info(accounts_iter)?, + &[BUS, &[5], &[args.bus_5_bump]], + )?, + load_uninitialized_pda( + next_account_info(accounts_iter)?, + &[BUS, &[6], &[args.bus_6_bump]], + )?, + load_uninitialized_pda( + next_account_info(accounts_iter)?, + &[BUS, &[7], &[args.bus_7_bump]], + )?, + ]; + + // Account 10: Mint + let mint = load_uninitialized_pda( + next_account_info(accounts_iter)?, + &[MINT, &[args.mint_bump]], + )?; + if !mint.key.eq(&MINT_ADDRESS) { + return Err(ProgramError::InvalidAccountData); + } + + // Account 11: Treasury + let treasury_account_info = load_uninitialized_pda( + next_account_info(accounts_iter)?, + &[TREASURY, &[args.treasury_bump]], + )?; + + // Account 12: Treasury tokens + let treasury_tokens = load_uninitialized_account(next_account_info(accounts_iter)?)?; + + // Account 13: System program + let system_program = load_account(next_account_info(accounts_iter)?, system_program::id())?; + + // Account 14: Token program + let token_program = load_account(next_account_info(accounts_iter)?, spl_token::id())?; + + // Account 15: Associated token program + let associated_token_program = load_account( + next_account_info(accounts_iter)?, + spl_associated_token_account::id(), + )?; + + // Account 16: Rent sysvar + let rent_sysvar = load_account(next_account_info(accounts_iter)?, sysvar::rent::id())?; + + // Initialize bus accounts + let bus_bumps = vec![ + args.bus_0_bump, + args.bus_1_bump, + args.bus_2_bump, + args.bus_3_bump, + args.bus_4_bump, + args.bus_5_bump, + args.bus_6_bump, + args.bus_7_bump, + ]; + for i in 0..BUS_COUNT { + create_pda( + busses[i], + &crate::id(), + size_of::(), + &[BUS, &[i as u8], &[bus_bumps[i]]], + system_program, + signer, + )?; + busses[i].try_borrow_mut_data()?.copy_from_slice( + Bus { + bump: bus_bumps[i] as u32, + id: i as u32, + available_rewards: 0, + } + .to_bytes(), + ); + } + + // Initialize treasury + create_pda( + treasury_account_info, + &crate::id(), + size_of::(), + &[TREASURY, &[args.treasury_bump]], + system_program, + signer, + )?; + let mut treasury_data = treasury_account_info.data.borrow_mut(); + let mut treasury = bytemuck::try_from_bytes_mut::(&mut treasury_data).unwrap(); + treasury.bump = args.treasury_bump as u64; + treasury.admin = *signer.key; + treasury.epoch_start_at = 0; + treasury.difficulty = INITIAL_DIFFICULTY.into(); + treasury.reward_rate = INITIAL_REWARD_RATE; + treasury.total_claimed_rewards = 0; + drop(treasury_data); + + // Initialize mint + create_pda( + mint, + &spl_token::id(), + Mint::LEN, + &[MINT, &[args.mint_bump]], + system_program, + signer, + )?; + solana_program::program::invoke_signed( + &spl_token::instruction::initialize_mint( + &spl_token::id(), + mint.key, + treasury_account_info.key, + None, + TOKEN_DECIMALS, + )?, + &[ + token_program.clone(), + mint.clone(), + treasury_account_info.clone(), + rent_sysvar.clone(), + ], + &[&[MINT, &[args.mint_bump]]], + )?; + + // Initialize treasury token account + solana_program::program::invoke( + &spl_associated_token_account::instruction::create_associated_token_account( + signer.key, + treasury_account_info.key, + mint.key, + &spl_token::id(), + ), + &[ + associated_token_program.clone(), + signer.clone(), + treasury_tokens.clone(), + treasury_account_info.clone(), + mint.clone(), + system_program.clone(), + token_program.clone(), + ], + )?; + + Ok(()) +} diff --git a/src/processor/mine.rs b/src/processor/mine.rs new file mode 100644 index 0000000..e49df35 --- /dev/null +++ b/src/processor/mine.rs @@ -0,0 +1,92 @@ +use solana_program::{ + account_info::AccountInfo, + entrypoint::ProgramResult, + keccak::{hashv, Hash}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +pub fn process_mine<'a, 'info>( + _program_id: &Pubkey, + accounts: &'a [AccountInfo<'info>], + data: &[u8], +) -> ProgramResult { + // TODO + Ok(()) +} + +pub(crate) fn validate_hash( + current_hash: Hash, + hash: Hash, + signer: Pubkey, + nonce: u64, + difficulty: Hash, +) -> Result<(), ProgramError> { + // Validate hash correctness. + let hash_ = hashv(&[ + current_hash.as_ref(), + signer.as_ref(), + nonce.to_be_bytes().as_slice(), + ]); + if !hash.eq(&hash_) { + return Err(ProgramError::Custom(1)); + } + + // Validate hash difficulty. + if !hash.le(&difficulty) { + return Err(ProgramError::Custom(1)); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use solana_program::{ + keccak::{hashv, Hash}, + pubkey::Pubkey, + }; + + use crate::validate_hash; + + #[test] + fn test_validate_hash_pass() { + let h1 = Hash::new_from_array([1; 32]); + let signer = Pubkey::new_unique(); + let nonce = 10u64; + let difficulty = Hash::new_from_array([255; 32]); + let h2 = hashv(&[ + h1.to_bytes().as_slice(), + signer.to_bytes().as_slice(), + nonce.to_be_bytes().as_slice(), + ]); + let res = validate_hash(h1, h2, signer, nonce, difficulty); + assert!(res.is_ok()); + } + + #[test] + fn test_validate_hash_fail() { + let h1 = Hash::new_from_array([1; 32]); + let signer = Pubkey::new_unique(); + let nonce = 10u64; + let difficulty = Hash::new_from_array([255; 32]); + let h2 = Hash::new_from_array([2; 32]); + let res = validate_hash(h1, h2, signer, nonce, difficulty); + assert!(res.is_err()); + } + + #[test] + fn test_validate_hash_fail_difficulty() { + let h1 = Hash::new_from_array([1; 32]); + let signer = Pubkey::new_unique(); + let nonce = 10u64; + let difficulty = Hash::new_from_array([0; 32]); + let h2 = hashv(&[ + h1.to_bytes().as_slice(), + signer.to_bytes().as_slice(), + nonce.to_be_bytes().as_slice(), + ]); + let res = validate_hash(h1, h2, signer, nonce, difficulty); + assert!(res.is_err()); + } +} diff --git a/src/processor/mod.rs b/src/processor/mod.rs new file mode 100644 index 0000000..da9c3c5 --- /dev/null +++ b/src/processor/mod.rs @@ -0,0 +1,11 @@ +mod claim; +mod epoch; +mod initialize; +mod mine; +mod proof; + +pub use claim::*; +pub use epoch::*; +pub use initialize::*; +pub use mine::*; +pub use proof::*; diff --git a/src/processor/proof.rs b/src/processor/proof.rs new file mode 100644 index 0000000..784e7a8 --- /dev/null +++ b/src/processor/proof.rs @@ -0,0 +1,10 @@ +use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; + +pub fn process_proof<'a, 'info>( + _program_id: &Pubkey, + accounts: &'a [AccountInfo<'info>], + data: &[u8], +) -> ProgramResult { + // TODO + Ok(()) +} diff --git a/src/state/bus.rs b/src/state/bus.rs new file mode 100644 index 0000000..bdc9a67 --- /dev/null +++ b/src/state/bus.rs @@ -0,0 +1,20 @@ +use bytemuck::{Pod, Zeroable}; + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +pub struct Bus { + /// The bump of the bus account PDA. + pub bump: u32, + + /// The ID of the bus account. + pub id: u32, + + /// The quantity of rewards this bus can issue in the current epoch epoch. + pub available_rewards: u64, +} + +impl Bus { + pub fn to_bytes(&self) -> &[u8] { + bytemuck::bytes_of(self) + } +} diff --git a/src/state/hash.rs b/src/state/hash.rs new file mode 100644 index 0000000..94d7902 --- /dev/null +++ b/src/state/hash.rs @@ -0,0 +1,20 @@ +use std::mem::transmute; + +use bytemuck::{Pod, Zeroable}; +use solana_program::keccak::{Hash as KeccakHash, HASH_BYTES}; + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +pub struct Hash(pub [u8; HASH_BYTES]); + +impl From for Hash { + fn from(value: KeccakHash) -> Self { + unsafe { transmute(value) } + } +} + +impl From for KeccakHash { + fn from(value: Hash) -> Self { + unsafe { transmute(value) } + } +} diff --git a/src/state/mod.rs b/src/state/mod.rs new file mode 100644 index 0000000..3a6040b --- /dev/null +++ b/src/state/mod.rs @@ -0,0 +1,7 @@ +mod bus; +mod hash; +mod treasury; + +pub use bus::*; +pub use hash::*; +pub use treasury::*; diff --git a/src/state/treasury.rs b/src/state/treasury.rs new file mode 100644 index 0000000..4bd12b9 --- /dev/null +++ b/src/state/treasury.rs @@ -0,0 +1,32 @@ +use bytemuck::{Pod, Zeroable}; +use solana_program::pubkey::Pubkey; + +use super::Hash; + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +pub struct Treasury { + /// The bump of the treasury account PDA. + pub bump: u64, + + /// The admin authority with permission to update the difficulty. + pub admin: Pubkey, + + /// The hash difficulty. + pub difficulty: Hash, + + /// The timestamp of the start of the current epoch. + pub epoch_start_at: i64, + + /// The reward rate to payout to miners for submiting valid hashes. + pub reward_rate: u64, + + /// The total lifetime claimed rewards. + pub total_claimed_rewards: u64, +} + +impl Treasury { + pub fn to_bytes(&self) -> &[u8] { + bytemuck::bytes_of(self) + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..c00aa6d --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,33 @@ +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey, rent::Rent, + sysvar::Sysvar, +}; + +/// Creates a new pda +#[inline(always)] +pub fn create_pda<'a, 'info>( + target_account: &'a AccountInfo<'info>, + owner: &Pubkey, + space: usize, + pda_seeds: &[&[u8]], + system_program: &'a AccountInfo<'info>, + payer: &'a AccountInfo<'info>, +) -> ProgramResult { + let rent = Rent::get()?; + solana_program::program::invoke_signed( + &solana_program::system_instruction::create_account( + payer.key, + target_account.key, + rent.minimum_balance(space as usize), + space as u64, + owner, + ), + &[ + payer.clone(), + target_account.clone(), + system_program.clone(), + ], + &[pda_seeds], + )?; + Ok(()) +} diff --git a/tests/test_initialize.rs b/tests/test_initialize.rs index 6a81305..7e782a3 100644 --- a/tests/test_initialize.rs +++ b/tests/test_initialize.rs @@ -1,6 +1,7 @@ use ore::{ instruction::{InitializeArgs, OreInstruction}, - Bus, Treasury, BUS, BUS_COUNT, INITIAL_REWARD_RATE, MINT, TREASURY, + state::{Bus, Treasury}, + BUS, BUS_COUNT, INITIAL_DIFFICULTY, INITIAL_REWARD_RATE, MINT, TREASURY, }; use solana_program::{ hash::Hash, @@ -15,7 +16,7 @@ use solana_sdk::{ signature::{Keypair, Signer}, transaction::Transaction, }; -use spl_token::state::Mint; +use spl_token::state::{AccountState, Mint}; #[tokio::test] async fn test_initialize() { @@ -56,6 +57,7 @@ async fn test_initialize() { AccountMeta::new(treasury_tokens_address, 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(sysvar::rent::id(), false), ], data: [ @@ -99,6 +101,7 @@ async fn test_initialize() { let treasury = bytemuck::try_from_bytes::(&treasury_account.data).unwrap(); assert_eq!(treasury.bump as u8, treasury_pda.1); assert_eq!(treasury.admin, payer.pubkey()); + assert_eq!(treasury.difficulty, INITIAL_DIFFICULTY.into()); assert_eq!(treasury.epoch_start_at as u8, 0); assert_eq!(treasury.reward_rate, INITIAL_REWARD_RATE); assert_eq!(treasury.total_claimed_rewards as u8, 0); @@ -112,6 +115,23 @@ async fn test_initialize() { assert_eq!(mint.decimals, ore::TOKEN_DECIMALS); assert_eq!(mint.is_initialized, true); assert_eq!(mint.freeze_authority, COption::None); + + // Test treasury token state + let treasury_tokens_account = banks + .get_account(treasury_tokens_address) + .await + .unwrap() + .unwrap(); + assert_eq!(treasury_tokens_account.owner, spl_token::id()); + let treasury_tokens = spl_token::state::Account::unpack(&treasury_tokens_account.data).unwrap(); + assert_eq!(treasury_tokens.mint, mint_pda.0); + assert_eq!(treasury_tokens.owner, treasury_pda.0); + assert_eq!(treasury_tokens.amount, 0); + assert_eq!(treasury_tokens.delegate, COption::None); + assert_eq!(treasury_tokens.state, AccountState::Initialized); + assert_eq!(treasury_tokens.is_native, COption::None); + assert_eq!(treasury_tokens.delegated_amount, 0); + assert_eq!(treasury_tokens.close_authority, COption::None); } async fn setup_program_test_env() -> (BanksClient, Keypair, Hash) {