diff --git a/src/loaders.rs b/src/loaders.rs index d175335..288ca60 100644 --- a/src/loaders.rs +++ b/src/loaders.rs @@ -5,7 +5,7 @@ use solana_program::{ use spl_token::state::Mint; use crate::{ - state::{Bus, Proof}, + state::{Bus, Proof, Treasury}, utils::AccountDeserialize, BUS_ADDRESSES, BUS_COUNT, MINT_ADDRESS, TREASURY_ADDRESS, }; @@ -21,11 +21,11 @@ pub fn load_signer<'a, 'info>(info: &'a AccountInfo<'info>) -> Result<(), Progra } /// Errors if: -/// - Account is not owned by Ore program. +/// - Owner is not Ore program. +/// - Address does not match the expected bus address. /// - Data is empty. /// - Data cannot deserialize into a bus account. /// - Bus ID does not match the expected ID. -/// - Address does not match the expected bus address. /// - Expected to be writable, but is not. pub fn load_bus<'a, 'info>( info: &'a AccountInfo<'info>, @@ -36,6 +36,10 @@ pub fn load_bus<'a, 'info>( return Err(ProgramError::InvalidAccountOwner); } + if info.key.ne(&BUS_ADDRESSES[id as usize]) { + return Err(ProgramError::InvalidSeeds); + } + if info.data_is_empty() { return Err(ProgramError::UninitializedAccount); } @@ -47,10 +51,6 @@ pub fn load_bus<'a, 'info>( return Err(ProgramError::InvalidAccountData); } - if info.key.ne(&BUS_ADDRESSES[id as usize]) { - return Err(ProgramError::InvalidAccountData); - } - if is_writable && !info.is_writable { return Err(ProgramError::InvalidAccountData); } @@ -59,7 +59,7 @@ pub fn load_bus<'a, 'info>( } /// Errors if: -/// - Account is not owned by Ore program. +/// - Owner is not Ore program. /// - Data is empty. /// - Data cannot deserialize into a bus account. /// - Bus ID is not in the expected range. @@ -96,7 +96,7 @@ pub fn load_any_bus<'a, 'info>( } /// Errors if: -/// - Account is not owned by Ore program. +/// - Owner is not Ore program. /// - Data is empty. /// - Data cannot deserialize into a proof account. /// - Proof authority does not match the expected address. @@ -129,10 +129,10 @@ pub fn load_proof<'a, 'info>( } /// Errors if: -/// - Account is not owned by Ore program. +/// - Owner is not Ore program. +/// - Address does not match the expected address. /// - Data is empty. /// - Data cannot deserialize into a treasury account. -/// - Address does not match the expected address. /// - Expected to be writable, but is not. pub fn load_treasury<'a, 'info>( info: &'a AccountInfo<'info>, @@ -142,13 +142,16 @@ pub fn load_treasury<'a, 'info>( return Err(ProgramError::InvalidAccountOwner); } + if info.key.ne(&TREASURY_ADDRESS) { + return Err(ProgramError::InvalidSeeds); + } + if info.data_is_empty() { return Err(ProgramError::UninitializedAccount); } - if info.key.ne(&TREASURY_ADDRESS) { - return Err(ProgramError::InvalidSeeds); - } + let treasury_data = info.data.borrow(); + let _ = Treasury::try_from_bytes(&treasury_data)?; if is_writable && !info.is_writable { return Err(ProgramError::InvalidAccountData); @@ -158,10 +161,10 @@ pub fn load_treasury<'a, 'info>( } /// Errors if: -/// - Account is not owned by SPL token program. +/// - Owner is not SPL token program. +/// - Address does not match the expected mint address. /// - Data is empty. /// - Data cannot deserialize into a mint account. -/// - Address does not match the expected mint address. /// - Expected to be writable, but is not. pub fn load_mint<'a, 'info>( info: &'a AccountInfo<'info>, @@ -171,6 +174,10 @@ pub fn load_mint<'a, 'info>( return Err(ProgramError::InvalidAccountOwner); } + if info.key.ne(&MINT_ADDRESS) { + return Err(ProgramError::InvalidSeeds); + } + if info.data_is_empty() { return Err(ProgramError::UninitializedAccount); } @@ -180,10 +187,6 @@ pub fn load_mint<'a, 'info>( return Err(ProgramError::InvalidAccountData); } - if info.key.ne(&MINT_ADDRESS) { - return Err(ProgramError::InvalidAccountData); - } - if is_writable && !info.is_writable { return Err(ProgramError::InvalidAccountData); } @@ -192,7 +195,7 @@ pub fn load_mint<'a, 'info>( } /// Errors if: -/// - Account is not owned by SPL token program. +/// - Owner is not SPL token program. /// - Data is empty. /// - Data cannot deserialize into a token account. /// - Token account owner does not match the expected owner address. @@ -256,7 +259,7 @@ pub fn load_uninitialized_pda<'a, 'info>( } /// Errors if: -/// - Account is not owned by the system program. +/// - Owner is not the system program. /// - Data is not empty. /// - Account is not writable. pub fn load_uninitialized_account<'a, 'info>( @@ -277,6 +280,7 @@ pub fn load_uninitialized_account<'a, 'info>( } /// Errors if: +/// - Owner is not the sysvar address. /// - Account cannot load with the expected address. pub fn load_sysvar<'a, 'info>( info: &'a AccountInfo<'info>, @@ -290,7 +294,7 @@ pub fn load_sysvar<'a, 'info>( } /// Errors if: -/// - Account does not match the expected value. +/// - Address does not match the expected value. /// - Expected to be writable, but is not. pub fn load_account<'a, 'info>( info: &'a AccountInfo<'info>, diff --git a/src/processor/initialize.rs b/src/processor/initialize.rs index b2f21fc..1f0b378 100644 --- a/src/processor/initialize.rs +++ b/src/processor/initialize.rs @@ -136,10 +136,10 @@ pub fn process_initialize<'a, 'info>( let mut treasury_data = treasury_info.data.borrow_mut(); treasury_data[0] = Treasury::discriminator() as u8; let treasury = Treasury::try_from_bytes_mut(&mut treasury_data)?; - treasury.bump = args.treasury_bump as u64; treasury.admin = *signer.key; - treasury.last_reset_at = 0; + treasury.bump = args.treasury_bump as u64; treasury.difficulty = INITIAL_DIFFICULTY.into(); + treasury.last_reset_at = 0; treasury.reward_rate = INITIAL_REWARD_RATE; treasury.total_claimed_rewards = 0; drop(treasury_data); diff --git a/src/processor/mine.rs b/src/processor/mine.rs index c0a9a63..f318e19 100644 --- a/src/processor/mine.rs +++ b/src/processor/mine.rs @@ -29,7 +29,8 @@ use crate::{ /// 4. Update the miner's lifetime stats. /// /// Safety requirements: -/// - Mine is a permissionless instruction and can be called by any miner. +/// - Mine is a permissionless instruction and can be called by any signer. +/// - Can only succeed if START_AT has passed. /// - Can only succeed if the last reset was less than 60 seconds ago. /// - Can only succeed if the provided SHA3 hash and nonce are valid and satisfy the difficulty. /// - The the provided proof account must be associated with the signer. @@ -70,8 +71,8 @@ pub fn process_mine<'a, 'info>( let mut proof_data = proof_info.data.borrow_mut(); let proof = Proof::try_from_bytes_mut(&mut proof_data)?; validate_hash( - proof.hash.into(), args.hash.into(), + proof.hash.into(), *signer.key, u64::from_le_bytes(args.nonce), treasury.difficulty.into(), @@ -106,8 +107,8 @@ pub fn process_mine<'a, 'info>( /// Validates the provided hash, ensursing it is equal to SHA3(current_hash, singer, nonce). /// Fails if the provided hash is valid but does not satisfy the required difficulty. pub(crate) fn validate_hash( - current_hash: KeccakHash, hash: KeccakHash, + current_hash: KeccakHash, signer: Pubkey, nonce: u64, difficulty: KeccakHash, @@ -150,7 +151,7 @@ mod tests { signer.to_bytes().as_slice(), nonce.to_le_bytes().as_slice(), ]); - let res = validate_hash(h1, h2, signer, nonce, difficulty); + let res = validate_hash(h2, h1, signer, nonce, difficulty); assert!(res.is_ok()); } @@ -161,7 +162,7 @@ mod tests { let nonce = 10u64; let difficulty = Hash::new_from_array([255; HASH_BYTES]); let h2 = Hash::new_from_array([2; HASH_BYTES]); - let res = validate_hash(h1, h2, signer, nonce, difficulty); + let res = validate_hash(h2, h1, signer, nonce, difficulty); assert!(res.is_err()); } @@ -176,7 +177,7 @@ mod tests { signer.to_bytes().as_slice(), nonce.to_le_bytes().as_slice(), ]); - let res = validate_hash(h1, h2, signer, nonce, difficulty); + let res = validate_hash(h2, h1, signer, nonce, difficulty); assert!(res.is_err()); } } diff --git a/src/processor/reset.rs b/src/processor/reset.rs index 25bd61b..ba9ddb4 100644 --- a/src/processor/reset.rs +++ b/src/processor/reset.rs @@ -18,14 +18,15 @@ use crate::{ /// 3. Top up the treasury token account to backup claims. /// /// Safety requirements: -/// - Reset is a permissionless crank function and can be invoked by anyone. +/// - Reset is a permissionless crank function and can be invoked by any signer. +/// - Can only succeed if START_AT has passed. /// - Can only succeed if more 60 seconds or more have passed since the last successful reset. /// - The busses, mint, treasury, treasury token account, and token program must all be valid. /// /// Discussion: -/// - It is critical that `reset` can only be invoked once per 60 second period to ensure the supply growth rate +/// - It is important that `reset` can only be invoked once per 60 second period to ensure the supply growth rate /// stays within the guaranteed bounds of 0 ≤ R ≤ 2 ORE/min. -/// - The reward rate is dynamically adjusted based on last epoch's actual reward rate (measured hashpower) to +/// - The reward rate is dynamically adjusted based on last epoch's actual reward rate (proxy hashpower) to /// target an average supply growth rate of 1 ORE/min. pub fn process_reset<'a, 'info>( _program_id: &Pubkey, diff --git a/tests/test_reset.rs b/tests/test_reset.rs index 6f231eb..701fbce 100644 --- a/tests/test_reset.rs +++ b/tests/test_reset.rs @@ -13,13 +13,15 @@ use solana_program::{ epoch_schedule::DEFAULT_SLOTS_PER_EPOCH, hash::Hash, instruction::{AccountMeta, Instruction}, + native_token::LAMPORTS_PER_SOL, program_option::COption, program_pack::Pack, pubkey::Pubkey, - sysvar, + system_program, sysvar, }; use solana_program_test::{processor, BanksClient, ProgramTest}; use solana_sdk::{ + account::Account, signature::{Keypair, Signer}, transaction::Transaction, }; @@ -28,7 +30,7 @@ use spl_token::state::{AccountState, Mint}; #[tokio::test] async fn test_reset() { // Setup - let (mut banks, payer, blockhash) = setup_program_test_env().await; + let (mut banks, payer, _, blockhash) = setup_program_test_env().await; // Pdas let bus_pdas = vec![ @@ -106,7 +108,7 @@ async fn test_reset() { #[tokio::test] async fn test_reset_busses_out_of_order_fail() { // Setup - let (mut banks, payer, blockhash) = setup_program_test_env().await; + let (mut banks, payer, _, blockhash) = setup_program_test_env().await; // Pdas let signer = payer.pubkey(); @@ -150,10 +152,33 @@ async fn test_reset_busses_out_of_order_fail() { assert!(res.is_err()); } +#[tokio::test] +async fn test_reset_early() { + // Setup + let (mut banks, payer, payer_alt, blockhash) = setup_program_test_env().await; + + // Reset one passes + let ix = ore::instruction::reset(payer.pubkey()); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + // Reset two fails + let ix = ore::instruction::reset(payer_alt.pubkey()); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer_alt.pubkey()), + &[&payer_alt], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_err()); +} + #[tokio::test] async fn test_reset_busses_duplicate_fail() { // Setup - let (mut banks, payer, blockhash) = setup_program_test_env().await; + let (mut banks, payer, _, blockhash) = setup_program_test_env().await; // Pdas let signer = payer.pubkey(); @@ -192,7 +217,7 @@ async fn test_reset_busses_duplicate_fail() { async fn test_reset_shuffle_error() { // Setup const FUZZ: u64 = 100; - let (mut banks, payer, blockhash) = setup_program_test_env().await; + let (mut banks, payer, _, blockhash) = setup_program_test_env().await; // Pdas let signer = payer.pubkey(); @@ -243,7 +268,7 @@ async fn test_reset_shuffle_error() { } } -async fn setup_program_test_env() -> (BanksClient, Keypair, Hash) { +async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { let mut program_test = ProgramTest::new("ore", ore::ID, processor!(ore::process_instruction)); program_test.prefer_bpf(true); @@ -346,5 +371,19 @@ async fn setup_program_test_env() -> (BanksClient, Keypair, Hash) { }, ); - program_test.start().await + // Setup alt payer + let payer_alt = Keypair::new(); + program_test.add_account( + payer_alt.pubkey(), + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let (banks, payer, blockhash) = program_test.start().await; + (banks, payer, payer_alt, blockhash) }