diff --git a/src/consts.rs b/src/consts.rs index 6fad3c4..92f437a 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -18,22 +18,16 @@ pub const MIN_DIFFICULTY: u32 = 8; /// There are 100 billion indivisible units per ORE (called "grains"). pub const TOKEN_DECIMALS: u8 = 11; -/// The decimal precision of the Ore v1 token. +/// The decimal precision of the ORE v1 token. pub const TOKEN_DECIMALS_V1: u8 = 9; -/// One Ore token, denominated in indivisible units. +/// One ORE token, denominated in indivisible units. pub const ONE_ORE: u64 = 10u64.pow(TOKEN_DECIMALS as u32); /// The duration of one minute, in seconds. pub const ONE_MINUTE: i64 = 60; -/// The duration of one day, in seconds. -pub const ONE_DAY: i64 = 86400; - -/// The duration of one year, in minutes. -pub const ONE_YEAR: u64 = 525600; - -/// The number of minutes in an ORE epoch. +/// The number of minutes in a program epoch. pub const EPOCH_MINUTES: i64 = 1; /// The duration of a program epoch, in seconds. diff --git a/src/instruction.rs b/src/instruction.rs index a265ab0..fdf9d2a 100644 --- a/src/instruction.rs +++ b/src/instruction.rs @@ -32,6 +32,13 @@ pub enum OreInstruction { #[account(3, name = "system_program", desc = "Solana system program")] Close = 1, + #[account(0, name = "ore_program", desc = "Ore program")] + #[account(1, name = "signer", desc = "Signer", signer)] + #[account(2, name = "config", desc = "Ore config account", writable)] + #[account(3, name = "proof", desc = "Ore proof account – current top staker")] + #[account(4, name = "proof_new", desc = "Ore proof account – new top staker")] + Crown = 2, + #[account(0, name = "ore_program", desc = "Ore program")] #[account(1, name = "signer", desc = "Signer", signer)] #[account(2, name = "bus", desc = "Ore bus account", writable)] @@ -39,13 +46,13 @@ pub enum OreInstruction { #[account(4, name = "noise", desc = "Ore noise account")] #[account(5, name = "proof", desc = "Ore proof account", writable)] #[account(6, name = "slot_hashes", desc = "Solana slot hashes sysvar")] - Mine = 2, + Mine = 3, #[account(0, name = "ore_program", desc = "Ore program")] #[account(1, name = "signer", desc = "Signer", signer)] #[account(2, name = "proof", desc = "Ore proof account", writable)] #[account(3, name = "system_program", desc = "Solana system program")] - Open = 3, + Open = 4, #[account(0, name = "ore_program", desc = "Ore program")] #[account(1, name = "signer", desc = "Signer", signer)] @@ -62,7 +69,7 @@ pub enum OreInstruction { #[account(12, name = "treasury", desc = "Ore treasury account", writable)] #[account(13, name = "treasury_tokens", desc = "Ore treasury token account", writable)] #[account(14, name = "token_program", desc = "SPL token program")] - Reset = 4, + Reset = 5, #[account(0, name = "ore_program", desc = "Ore program")] #[account(1, name = "signer", desc = "Signer", signer)] @@ -70,12 +77,12 @@ pub enum OreInstruction { #[account(3, name = "sender", desc = "Signer token account", writable)] #[account(4, name = "treasury_tokens", desc = "Ore treasury token account", writable)] #[account(5, name = "token_program", desc = "SPL token program")] - Stake = 5, + Stake = 6, #[account(0, name = "ore_program", desc = "Ore program")] #[account(1, name = "signer", desc = "Signer", signer)] #[account(2, name = "proof", desc = "Ore proof account", writable)] - Update = 6, + Update = 7, #[account(0, name = "ore_program", desc = "Ore program")] #[account(1, name = "signer", desc = "Signer", signer)] @@ -85,7 +92,7 @@ pub enum OreInstruction { #[account(5, name = "mint", desc = "Ore token mint account", writable)] #[account(6, name = "mint_v1", desc = "Ore v1 token mint account", writable)] #[account(7, name = "token_program", desc = "SPL token program")] - Upgrade = 7, + Upgrade = 8, #[account(0, name = "ore_program", desc = "Ore program")] #[account(1, name = "signer", desc = "Admin signer", signer)] diff --git a/src/lib.rs b/src/lib.rs index a6cf811..51ede90 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,7 @@ pub fn process_instruction( match OreInstruction::try_from(*tag).or(Err(ProgramError::InvalidInstructionData))? { OreInstruction::Claim => process_claim(program_id, accounts, data)?, OreInstruction::Close => process_close(program_id, accounts, data)?, + OreInstruction::Crown => process_crown(program_id, accounts, data)?, OreInstruction::Mine => process_mine(program_id, accounts, data)?, OreInstruction::Open => process_open(program_id, accounts, data)?, OreInstruction::Reset => process_reset(program_id, accounts, data)?, diff --git a/src/loaders.rs b/src/loaders.rs index 44a3ffe..dd53166 100644 --- a/src/loaders.rs +++ b/src/loaders.rs @@ -191,6 +191,34 @@ pub fn load_proof_with_miner<'a, 'info>( Ok(()) } +/// Errors if: +/// - Owner is not Ore program. +/// - Data is empty. +/// - Data cannot deserialize into a proof account. +/// - Expected to be writable, but is not. +pub fn load_any_proof<'a, 'info>( + info: &'a AccountInfo<'info>, + is_writable: bool, +) -> Result<(), ProgramError> { + if info.owner.ne(&crate::id()) { + return Err(ProgramError::InvalidAccountOwner); + } + + if info.data_is_empty() { + return Err(ProgramError::UninitializedAccount); + } + + if info.data.borrow()[0].ne(&(Proof::discriminator() as u8)) { + return Err(solana_program::program_error::ProgramError::InvalidAccountData); + } + + if is_writable && !info.is_writable { + return Err(ProgramError::InvalidAccountData); + } + + Ok(()) +} + /// Errors if: /// - Owner is not Ore program. /// - Address does not match the expected address. diff --git a/src/processor/crown.rs b/src/processor/crown.rs new file mode 100644 index 0000000..674f114 --- /dev/null +++ b/src/processor/crown.rs @@ -0,0 +1,57 @@ +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError, + pubkey::Pubkey, +}; + +use crate::{ + loaders::*, + state::{Config, Proof}, + utils::AccountDeserialize, +}; + +/// Crown marks an account as the top staker if their balance is greater than the last known top staker. +pub fn process_crown<'a, 'info>( + _program_id: &Pubkey, + accounts: &'a [AccountInfo<'info>], + _data: &[u8], +) -> ProgramResult { + // Load accounts + let [signer, config_info, proof_info, proof_new_info] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + load_signer(signer)?; + 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 + let proof_new_data = proof_new_info.data.borrow(); + let proof_new = Proof::try_from_bytes(&proof_new_data)?; + + // If top staker is not the default null address, then compare balances + if config.top_staker.ne(&Pubkey::new_from_array([0; 32])) { + // Load current top staker + load_any_proof(proof_info, false)?; + let proof_data = proof_info.data.borrow(); + let proof = Proof::try_from_bytes(&proof_data)?; + + // Require the provided proof account is the current top staker + if config.top_staker.ne(&proof_info.key) { + return Ok(()); + } + + // Compare balances + if proof_new.balance.lt(&proof.balance) { + return Ok(()); + } + } + + // Crown the new top staker + config.max_stake = proof_new.balance; + config.top_staker = *proof_new_info.key; + + Ok(()) +} diff --git a/src/processor/initialize.rs b/src/processor/initialize.rs index 7c0b0af..fa614eb 100644 --- a/src/processor/initialize.rs +++ b/src/processor/initialize.rs @@ -138,6 +138,8 @@ pub fn process_initialize<'a, 'info>( config.admin = *signer.key; config.base_reward_rate = INITIAL_BASE_REWARD_RATE; config.last_reset_at = 0; + config.max_stake = 0; + config.top_staker = Pubkey::new_from_array([0; 32]); // Initialize treasury create_pda( diff --git a/src/processor/mine.rs b/src/processor/mine.rs index a3df1c8..e66b8aa 100644 --- a/src/processor/mine.rs +++ b/src/processor/mine.rs @@ -24,7 +24,7 @@ use crate::{ loaders::*, state::{Bus, Config, Proof}, utils::{AccountDeserialize, MineEvent}, - EPOCH_DURATION, MIN_DIFFICULTY, ONE_MINUTE, ONE_YEAR, TOLERANCE, + EPOCH_DURATION, MIN_DIFFICULTY, ONE_MINUTE, TOLERANCE, }; /// Mine is the primary workhorse instruction of the Ore program. Its responsibilities include: @@ -61,12 +61,12 @@ pub fn process_mine<'a, 'info>( load_sysvar(instructions_sysvar, sysvar::instructions::id())?; load_sysvar(slot_hashes_sysvar, sysvar::slot_hashes::id())?; - // Validate this is the only mine ix in the transaction + // Validate this is the only mine ix in the transaction. if !validate_transaction(&instructions_sysvar.data.borrow()).unwrap_or(false) { return Err(OreError::TransactionInvalid.into()); } - // Validate epoch is active + // Validate epoch is active. let config_data = config_info.data.borrow(); let config = Config::try_from_bytes(&config_data)?; let clock = Clock::get().or(Err(ProgramError::InvalidAccountData))?; @@ -78,7 +78,7 @@ pub fn process_mine<'a, 'info>( return Err(OreError::NeedsReset.into()); } - // Validate the digest + // Validate the hash digest. let mut proof_data = proof_info.data.borrow_mut(); let proof = Proof::try_from_bytes_mut(&mut proof_data)?; let solution = Solution::new(args.digest, args.nonce); @@ -86,7 +86,7 @@ pub fn process_mine<'a, 'info>( return Err(OreError::HashInvalid.into()); } - // Validate hash satisfies the minimnum difficulty + // Validate hash satisfies the minimnum difficulty. let hash = solution.to_hash(); let difficulty = hash.difficulty(); sol_log(&format!("Diff {}", difficulty)); @@ -94,42 +94,39 @@ pub fn process_mine<'a, 'info>( return Err(OreError::HashTooEasy.into()); } - // Calculate base reward rate + // Calculate base reward rate. let difficulty = difficulty.saturating_sub(MIN_DIFFICULTY); let mut reward = config .base_reward_rate .saturating_mul(2u64.saturating_pow(difficulty)); - sol_log(&format!("Base {}", reward)); // Apply staking multiplier. - // The multiplier can range 1x to 2x. To receive the maximum multiplier, the stake balance must be - // greater than or equal to two years worth of rewards at the selected difficulty. Miners are only - // eligable for a multipler if their last stake deposit was more than one minute ago. - if proof - .last_stake_at - .saturating_add(ONE_MINUTE) - .le(&clock.unix_timestamp) + // 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 upper_bound = reward.saturating_mul(ONE_YEAR); let staking_reward = proof .balance - .min(upper_bound) + .min(config.max_stake) .saturating_mul(reward) - .saturating_div(upper_bound); + .saturating_div(config.max_stake); reward = reward.saturating_add(staking_reward); - sol_log(&format!("Staking {}", staking_reward)); - }; + } - // Apply spam penalty + // 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) { - sol_log("Spam penalty"); return Err(OreError::Spam.into()); } - // Apply liveness penalty + // Apply liveness penalty. let t_liveness = t_target.saturating_add(TOLERANCE); if t.gt(&t_liveness) { reward = reward.saturating_sub( @@ -137,11 +134,6 @@ pub fn process_mine<'a, 'info>( .saturating_mul(t.saturating_sub(t_liveness) as u64) .saturating_div(ONE_MINUTE as u64), ); - sol_log(&format!( - "Liveness penalty ({} sec) {}", - t.saturating_sub(t_liveness), - reward, - )); } // Limit payout amount to whatever is left in the bus @@ -150,8 +142,6 @@ pub fn process_mine<'a, 'info>( let reward_actual = reward.min(bus.rewards); // Update balances - sol_log(&format!("Total {}", reward)); - sol_log(&format!("Bus {}", bus.rewards)); 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); @@ -215,9 +205,7 @@ fn validate_transaction(msg: &[u8]) -> Result { return Ok(false); } } - COMPUTE_BUDGET_PROGRAM_ID => { - // Noop - } + COMPUTE_BUDGET_PROGRAM_ID => {} // Noop _ => return Ok(false), } } diff --git a/src/processor/mod.rs b/src/processor/mod.rs index 5ded886..e46eafe 100644 --- a/src/processor/mod.rs +++ b/src/processor/mod.rs @@ -1,5 +1,6 @@ mod claim; mod close; +mod crown; mod initialize; mod mine; mod open; @@ -10,6 +11,7 @@ mod upgrade; pub use claim::*; pub use close::*; +pub use crown::*; pub use initialize::*; pub use mine::*; pub use open::*; diff --git a/src/state/bus.rs b/src/state/bus.rs index 36b3479..7275119 100644 --- a/src/state/bus.rs +++ b/src/state/bus.rs @@ -8,7 +8,6 @@ use crate::{ /// Bus accounts are responsible for distributing mining rewards. /// There are 8 busses total to minimize write-lock contention and allow for parallel mine operations. -/// Every epoch, the bus account rewards counters are topped up to 0.25 ORE each (2 ORE split amongst 8 busses). #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, Pod, ShankAccount, Zeroable)] pub struct Bus { diff --git a/src/state/config.rs b/src/state/config.rs index 9c79cff..2c8214f 100644 --- a/src/state/config.rs +++ b/src/state/config.rs @@ -17,8 +17,14 @@ pub struct Config { /// The base reward rate paid out for a hash of minimum difficulty. pub base_reward_rate: u64, - /// The timestamp of the last reset + /// The timestamp of the last reset. pub last_reset_at: i64, + + /// The largest known stake balance on the network. + pub max_stake: u64, + + /// The address of the proof account with the highest stake balance. + pub top_staker: Pubkey, } impl Discriminator for Config {