diff --git a/api/src/consts.rs b/api/src/consts.rs index d0d7fae..2161b55 100644 --- a/api/src/consts.rs +++ b/api/src/consts.rs @@ -31,20 +31,20 @@ pub const ONE_MINUTE: i64 = 60; pub const EPOCH_MINUTES: i64 = 1; /// The duration of a program epoch, in seconds. -pub const EPOCH_DURATION: i64 = ONE_MINUTE.saturating_mul(EPOCH_MINUTES); +pub const EPOCH_DURATION: i64 = ONE_MINUTE * EPOCH_MINUTES; /// The maximum token supply (21 million). -pub const MAX_SUPPLY: u64 = ONE_ORE.saturating_mul(21_000_000); +pub const MAX_SUPPLY: u64 = ONE_ORE * 21_000_000; /// The target quantity of ORE to be mined per epoch. -pub const TARGET_EPOCH_REWARDS: u64 = ONE_ORE.saturating_mul(EPOCH_MINUTES as u64); +pub const TARGET_EPOCH_REWARDS: u64 = ONE_ORE * EPOCH_MINUTES as u64; /// The maximum quantity of ORE that can be mined per epoch. /// Inflation rate ≈ 1 ORE / min (min 0, max 8) -pub const MAX_EPOCH_REWARDS: u64 = TARGET_EPOCH_REWARDS.saturating_mul(BUS_COUNT as u64); +pub const MAX_EPOCH_REWARDS: u64 = TARGET_EPOCH_REWARDS * BUS_COUNT as u64; /// The quantity of ORE each bus is allowed to issue per epoch. -pub const BUS_EPOCH_REWARDS: u64 = MAX_EPOCH_REWARDS.saturating_div(BUS_COUNT as u64); +pub const BUS_EPOCH_REWARDS: u64 = MAX_EPOCH_REWARDS / BUS_COUNT as u64; /// The number of bus accounts, for parallelizing mine operations. pub const BUS_COUNT: usize = 8; diff --git a/api/src/error.rs b/api/src/error.rs index a0fdbf9..e3d8ee0 100644 --- a/api/src/error.rs +++ b/api/src/error.rs @@ -23,6 +23,10 @@ pub enum OreError { ToleranceOverflow = 7, #[error("The maximum supply has been reached")] MaxSupply = 8, + #[error("This account cannot be closed because it's the top staker")] + CannotClose = 9, + #[error("This account cannot be crowned because its last stake was too recent")] + CannotCrown = 10, } impl From for ProgramError { diff --git a/api/src/loaders.rs b/api/src/loaders.rs index 998dca6..90f13ed 100644 --- a/api/src/loaders.rs +++ b/api/src/loaders.rs @@ -289,9 +289,7 @@ pub fn load_mint<'a, 'info>( return Err(ProgramError::UninitializedAccount); } - if Mint::unpack_unchecked(&info.data.borrow()).is_err() { - return Err(ProgramError::InvalidAccountData); - } + Mint::unpack(&info.data.borrow())?; if is_writable && !info.is_writable { return Err(ProgramError::InvalidAccountData); @@ -322,8 +320,7 @@ pub fn load_token_account<'a, 'info>( } let account_data = info.data.borrow(); - let account = spl_token::state::Account::unpack_unchecked(&account_data) - .or(Err(ProgramError::InvalidAccountData))?; + let account = spl_token::state::Account::unpack(&account_data)?; if account.mint.ne(&mint) { return Err(ProgramError::InvalidAccountData); diff --git a/program/src/close.rs b/program/src/close.rs index d92bb20..052f87d 100644 --- a/program/src/close.rs +++ b/program/src/close.rs @@ -1,4 +1,8 @@ -use ore_api::{loaders::*, state::Proof}; +use ore_api::{ + error::OreError, + loaders::*, + state::{Config, Proof}, +}; use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError, pubkey::Pubkey, system_program, @@ -20,13 +24,21 @@ pub fn process_close<'a, 'info>( _data: &[u8], ) -> ProgramResult { // Load accounts - let [signer, proof_info, system_program] = accounts else { + let [signer, config_info, proof_info, system_program] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; load_signer(signer)?; + load_config(config_info, false)?; load_proof(proof_info, signer.key, true)?; load_program(system_program, system_program::id())?; + // Validate the account is not the crowned top staker. + let config_data = config_info.data.borrow(); + let config = Config::try_from_bytes(&config_data)?; + if config.top_staker.eq(proof_info.key) { + return Err(OreError::CannotClose.into()); + } + // Validate balance is zero let proof_data = proof_info.data.borrow(); let proof = Proof::try_from_bytes(&proof_data)?; diff --git a/program/src/crown.rs b/program/src/crown.rs index 39e3d93..8849f9c 100644 --- a/program/src/crown.rs +++ b/program/src/crown.rs @@ -1,10 +1,12 @@ use ore_api::{ + consts::ONE_MINUTE, + error::OreError, loaders::*, state::{Config, Proof}, }; use solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError, - pubkey::Pubkey, + account_info::AccountInfo, clock::Clock, entrypoint::ProgramResult, + program_error::ProgramError, pubkey::Pubkey, sysvar::Sysvar, }; use crate::utils::AccountDeserialize; @@ -23,15 +25,23 @@ pub fn process_crown<'a, 'info>( load_config(config_info, true)?; load_any_proof(proof_new_info, false)?; - // Load config - let mut config_data = config_info.data.borrow_mut(); - let config = Config::try_from_bytes_mut(&mut config_data)?; - - // Load proposed new top staker + // Load the proof accounts. + let clock = Clock::get().unwrap(); let proof_new_data = proof_new_info.data.borrow(); let proof_new = Proof::try_from_bytes(&proof_new_data)?; + if proof_new + .last_stake_at + .saturating_add(ONE_MINUTE) + .gt(&clock.unix_timestamp) + { + return Err(OreError::CannotCrown.into()); + } - // If top staker is the defualt null balance, skip this. + // If top staker is the default null address, skip this. + let mut config_data = config_info.data.borrow_mut(); + let config = Config::try_from_bytes_mut(&mut config_data)?; + let proof_data = proof_info.data.borrow(); + let proof = Proof::try_from_bytes(&proof_data)?; if config.top_staker.ne(&Pubkey::new_from_array([0; 32])) { // Load current top staker load_any_proof(proof_info, false)?; @@ -40,14 +50,12 @@ pub fn process_crown<'a, 'info>( } // Compare balances - let proof_data = proof_info.data.borrow(); - let proof = Proof::try_from_bytes(&proof_data)?; if proof_new.balance.lt(&proof.balance) { return Ok(()); } } - // Crown the new top staker + // Crown the new top staker. config.max_stake = proof_new.balance; config.top_staker = *proof_new_info.key; diff --git a/program/src/mine.rs b/program/src/mine.rs index 8ef0347..4fec55b 100644 --- a/program/src/mine.rs +++ b/program/src/mine.rs @@ -18,7 +18,6 @@ use solana_program::{ entrypoint::ProgramResult, log::sol_log, program_error::ProgramError, - pubkey, pubkey::Pubkey, sanitize::SanitizeError, serialize_utils::{read_pubkey, read_u16, read_u8}, @@ -63,7 +62,7 @@ pub fn process_mine<'a, 'info>( load_sysvar(slot_hashes_sysvar, sysvar::slot_hashes::id())?; // Validate this is the only mine ix in the transaction. - if !validate_transaction(&instructions_sysvar.data.borrow()).unwrap_or(false) { + if !introspect_transaction(&instructions_sysvar.data.borrow()).unwrap_or(false) { return Err(OreError::TransactionInvalid.into()); } @@ -94,33 +93,28 @@ pub fn process_mine<'a, 'info>( if difficulty.lt(&MIN_DIFFICULTY) { return Err(OreError::HashTooEasy.into()); } - - // Calculate base reward rate. - let difficulty = difficulty.saturating_sub(MIN_DIFFICULTY); let mut reward = config .base_reward_rate - .saturating_mul(2u64.saturating_pow(difficulty)); + .checked_mul(2u64.checked_pow(difficulty).unwrap()) + .unwrap(); // Apply staking multiplier. // If user has greater than or equal to the max stake on the network, they receive 2x multiplier. // Any stake less than this will receives between 1x and 2x multipler. The multipler is only active // if the miner's last stake deposit was more than one minute ago. - if config.max_stake.gt(&0) - && proof - .last_stake_at - .saturating_add(ONE_MINUTE) - .le(&clock.unix_timestamp) - { + let t = clock.unix_timestamp; + if config.max_stake.gt(&0) && proof.last_stake_at.saturating_add(ONE_MINUTE).le(&t) { let staking_reward = proof .balance .min(config.max_stake) - .saturating_mul(reward) - .saturating_div(config.max_stake); - reward = reward.saturating_add(staking_reward); + .checked_mul(reward) + .unwrap() + .checked_div(config.max_stake) + .unwrap(); + reward = reward.checked_add(staking_reward).unwrap(); } // Reject spam transactions. - let t = clock.unix_timestamp; let t_target = proof.last_hash_at.saturating_add(ONE_MINUTE); let t_spam = t_target.saturating_sub(TOLERANCE); if t.lt(&t_spam) { @@ -130,11 +124,15 @@ pub fn process_mine<'a, 'info>( // Apply liveness penalty. let t_liveness = t_target.saturating_add(TOLERANCE); if t.gt(&t_liveness) { - reward = reward.saturating_sub( - reward - .saturating_mul(t.saturating_sub(t_liveness) as u64) - .saturating_div(ONE_MINUTE as u64), - ); + reward = reward + .checked_sub( + reward + .checked_mul(t.checked_sub(t_liveness).unwrap() as u64) + .unwrap() + .checked_div(ONE_MINUTE as u64) + .unwrap(), + ) + .unwrap(); } // Limit payout amount to whatever is left in the bus @@ -143,9 +141,9 @@ pub fn process_mine<'a, 'info>( let reward_actual = reward.min(bus.rewards); // Update balances - bus.theoretical_rewards = bus.theoretical_rewards.saturating_add(reward); - bus.rewards = bus.rewards.saturating_sub(reward_actual); - proof.balance = proof.balance.saturating_add(reward_actual); + bus.theoretical_rewards = bus.theoretical_rewards.checked_add(reward).unwrap(); + bus.rewards = bus.rewards.checked_sub(reward_actual).unwrap(); + proof.balance = proof.balance.checked_add(reward_actual).unwrap(); // Hash recent slot hash into the next challenge to prevent pre-mining attacks proof.last_hash = hash.h; @@ -156,10 +154,7 @@ pub fn process_mine<'a, 'info>( .0; // Update time trackers - proof.last_hash_at = proof - .last_hash_at - .saturating_add(ONE_MINUTE) - .max(clock.unix_timestamp); + proof.last_hash_at = t.max(t_target); // Update lifetime stats proof.total_hashes = proof.total_hashes.saturating_add(1); @@ -187,7 +182,7 @@ pub fn process_mine<'a, 'info>( /// /// If each transaction is limited to one hash only, then a user will minimize their fee / hash /// by allocating all their hashpower to finding the single most difficult hash they can. -fn validate_transaction(msg: &[u8]) -> Result { +fn introspect_transaction(msg: &[u8]) -> Result { #[allow(deprecated)] let idx = load_current_index(msg); let mut c = 0; @@ -198,27 +193,29 @@ fn validate_transaction(msg: &[u8]) -> Result { c = read_u16(&mut c, msg)? as usize; let num_accounts = read_u16(&mut c, msg)? as usize; c += num_accounts * 33; - // Only allow instructions to call ore and the compute budget program. - match read_pubkey(&mut c, msg)? { - ore_api::ID => { - c += 2; - if let Ok(ix) = OreInstruction::try_from(read_u8(&mut c, msg)?) { - if let OreInstruction::Mine = ix { - if i.ne(&(idx as usize)) { - return Ok(false); - } - } - } else { + let program_id = read_pubkey(&mut c, msg)?; + if i.eq(&(idx as usize)) { + // Require top-level instruction at current index is a `mine` + if program_id.ne(&ore_api::ID) { + return Ok(false); + } + if let Ok(ix) = OreInstruction::try_from(read_u8(&mut c, msg)?) { + if ix.ne(&OreInstruction::Mine) { return Ok(false); } } - COMPUTE_BUDGET_PROGRAM_ID => {} // Noop - _ => return Ok(false), + } else { + // Require no other instructions in the transaction are a `mine` + if program_id.eq(&ore_api::ID) { + c += 2; + if let Ok(ix) = OreInstruction::try_from(read_u8(&mut c, msg)?) { + if ix.eq(&OreInstruction::Mine) { + return Ok(false); + } + } + } } } Ok(true) } - -/// Program id of the compute budge program. -const COMPUTE_BUDGET_PROGRAM_ID: Pubkey = pubkey!("ComputeBudget111111111111111111111111111111"); diff --git a/program/src/stake.rs b/program/src/stake.rs index 48cfb6f..10f43e0 100644 --- a/program/src/stake.rs +++ b/program/src/stake.rs @@ -36,7 +36,7 @@ pub fn process_stake<'a, 'info>( // Update proof balance let mut proof_data = proof_info.data.borrow_mut(); let proof = Proof::try_from_bytes_mut(&mut proof_data)?; - proof.balance = proof.balance.saturating_add(amount); + proof.balance = proof.balance.checked_add(amount).unwrap(); // Update deposit timestamp let clock = Clock::get().or(Err(ProgramError::InvalidAccountData))?;