From 77bf1951395fa9bbf0167d0174a4e96ba7ec7a9b Mon Sep 17 00:00:00 2001 From: Hardhat Chad Date: Thu, 27 Jun 2024 13:17:53 +0000 Subject: [PATCH 01/10] begin testing new staking mechanism --- src/processor/mine.rs | 22 +++++++++++----------- src/processor/stake.rs | 17 ++++++++++++++--- src/state/bus.rs | 1 - src/state/config.rs | 5 ++++- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/processor/mine.rs b/src/processor/mine.rs index 265bc61..b327e33 100644 --- a/src/processor/mine.rs +++ b/src/processor/mine.rs @@ -102,23 +102,23 @@ pub fn process_mine<'a, 'info>( 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 will receive 2x multiplier. + // Any less than this, and they will receive between 1x and 2x. Miners are only eligable for a multipler + // if their 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 let t = clock.unix_timestamp; diff --git a/src/processor/stake.rs b/src/processor/stake.rs index 79be3dd..d988520 100644 --- a/src/processor/stake.rs +++ b/src/processor/stake.rs @@ -4,8 +4,11 @@ use solana_program::{ }; use crate::{ - instruction::StakeArgs, loaders::*, state::Proof, utils::AccountDeserialize, MINT_ADDRESS, - TREASURY_ADDRESS, + instruction::StakeArgs, + loaders::*, + state::{Config, Proof}, + utils::AccountDeserialize, + MINT_ADDRESS, TREASURY_ADDRESS, }; /// Stake deposits Ore into a miner's proof account to earn multiplier. Its responsibilies include: @@ -26,10 +29,13 @@ pub fn process_stake<'a, 'info>( let amount = u64::from_le_bytes(args.amount); // Load accounts - let [signer, proof_info, sender_info, treasury_tokens_info, token_program] = accounts else { + let [signer, config_info, proof_info, sender_info, treasury_tokens_info, token_program] = + accounts + else { return Err(ProgramError::NotEnoughAccountKeys); }; load_signer(signer)?; + load_config(config_info, true)?; load_proof(proof_info, signer.key, true)?; load_token_account(sender_info, Some(signer.key), &MINT_ADDRESS, true)?; load_token_account( @@ -49,6 +55,11 @@ pub fn process_stake<'a, 'info>( let clock = Clock::get().or(Err(ProgramError::InvalidAccountData))?; proof.last_stake_at = clock.unix_timestamp; + // Update the max stake tracker + let mut config_data = config_info.data.borrow_mut(); + let config = Config::try_from_bytes_mut(&mut config_data)?; + config.max_stake = config.max_stake.max(proof.balance); + // Distribute tokens from signer to treasury solana_program::program::invoke( &spl_token::instruction::transfer( 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..b3de137 100644 --- a/src/state/config.rs +++ b/src/state/config.rs @@ -17,8 +17,11 @@ 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 stake account on the network. + pub max_stake: u64, } impl Discriminator for Config { From 3608071f1cae693ac9d901e49c5e2effa253e188 Mon Sep 17 00:00:00 2001 From: Hardhat Chad Date: Thu, 27 Jun 2024 14:01:52 +0000 Subject: [PATCH 02/10] crown new top staker --- src/consts.rs | 6 ---- src/instruction.rs | 9 +++++- src/lib.rs | 1 + src/loaders.rs | 28 ++++++++++++++++++ src/processor/crown.rs | 57 +++++++++++++++++++++++++++++++++++++ src/processor/initialize.rs | 2 ++ src/processor/mine.rs | 2 +- src/processor/mod.rs | 2 ++ src/processor/stake.rs | 17 ++--------- src/state/config.rs | 5 +++- 10 files changed, 106 insertions(+), 23 deletions(-) create mode 100644 src/processor/crown.rs diff --git a/src/consts.rs b/src/consts.rs index fdc8e35..6d6f2df 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -24,12 +24,6 @@ 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. pub const EPOCH_MINUTES: i64 = 1; diff --git a/src/instruction.rs b/src/instruction.rs index 631fed0..d237f6e 100644 --- a/src/instruction.rs +++ b/src/instruction.rs @@ -72,6 +72,13 @@ pub enum OreInstruction { #[account(5, name = "token_program", desc = "SPL token program")] Stake = 5, + #[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 = 6, + #[account(0, name = "ore_program", desc = "Ore program")] #[account(1, name = "signer", desc = "Signer", signer)] #[account(2, name = "beneficiary", desc = "Beneficiary token account", writable)] @@ -80,7 +87,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 = 6, + Upgrade = 7, #[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 a5d064d..5b2b465 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::Open => process_open(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::Claim => process_claim(program_id, accounts, data)?, OreInstruction::Stake => process_stake(program_id, accounts, data)?, diff --git a/src/loaders.rs b/src/loaders.rs index 73d7c50..449d369 100644 --- a/src/loaders.rs +++ b/src/loaders.rs @@ -158,6 +158,34 @@ pub fn load_proof<'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..5d18ecb --- /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 proof_info.key.ne(&config.top_staker) { + return Err(ProgramError::InvalidAccountData); + } + + // Compare balances + if proof_new.balance.lt(&proof.balance) { + return Err(ProgramError::InvalidAccountData); + } + } + + // 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 7717a5f..2ee2aac 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 b327e33..b1e4fd9 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: diff --git a/src/processor/mod.rs b/src/processor/mod.rs index 91b8fb4..11469b5 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; @@ -9,6 +10,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/processor/stake.rs b/src/processor/stake.rs index d988520..79be3dd 100644 --- a/src/processor/stake.rs +++ b/src/processor/stake.rs @@ -4,11 +4,8 @@ use solana_program::{ }; use crate::{ - instruction::StakeArgs, - loaders::*, - state::{Config, Proof}, - utils::AccountDeserialize, - MINT_ADDRESS, TREASURY_ADDRESS, + instruction::StakeArgs, loaders::*, state::Proof, utils::AccountDeserialize, MINT_ADDRESS, + TREASURY_ADDRESS, }; /// Stake deposits Ore into a miner's proof account to earn multiplier. Its responsibilies include: @@ -29,13 +26,10 @@ pub fn process_stake<'a, 'info>( let amount = u64::from_le_bytes(args.amount); // Load accounts - let [signer, config_info, proof_info, sender_info, treasury_tokens_info, token_program] = - accounts - else { + let [signer, proof_info, sender_info, treasury_tokens_info, token_program] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; load_signer(signer)?; - load_config(config_info, true)?; load_proof(proof_info, signer.key, true)?; load_token_account(sender_info, Some(signer.key), &MINT_ADDRESS, true)?; load_token_account( @@ -55,11 +49,6 @@ pub fn process_stake<'a, 'info>( let clock = Clock::get().or(Err(ProgramError::InvalidAccountData))?; proof.last_stake_at = clock.unix_timestamp; - // Update the max stake tracker - let mut config_data = config_info.data.borrow_mut(); - let config = Config::try_from_bytes_mut(&mut config_data)?; - config.max_stake = config.max_stake.max(proof.balance); - // Distribute tokens from signer to treasury solana_program::program::invoke( &spl_token::instruction::transfer( diff --git a/src/state/config.rs b/src/state/config.rs index b3de137..2c8214f 100644 --- a/src/state/config.rs +++ b/src/state/config.rs @@ -20,8 +20,11 @@ pub struct Config { /// The timestamp of the last reset. pub last_reset_at: i64, - /// The largest stake account on the network. + /// 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 { From c2ef73a6d00c70763110e30d58b80efeb483801b Mon Sep 17 00:00:00 2001 From: Hardhat Chad Date: Thu, 27 Jun 2024 14:03:02 +0000 Subject: [PATCH 03/10] if --- src/processor/crown.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/processor/crown.rs b/src/processor/crown.rs index 5d18ecb..984fdf7 100644 --- a/src/processor/crown.rs +++ b/src/processor/crown.rs @@ -39,7 +39,7 @@ pub fn process_crown<'a, 'info>( let proof = Proof::try_from_bytes(&proof_data)?; // Require the provided proof account is the current top staker - if proof_info.key.ne(&config.top_staker) { + if config.top_staker.ne(&proof_info.key) { return Err(ProgramError::InvalidAccountData); } From d5e785899f3f027478c611f46870d6051575c169 Mon Sep 17 00:00:00 2001 From: Hardhat Chad Date: Thu, 27 Jun 2024 14:46:14 +0000 Subject: [PATCH 04/10] silent error --- src/processor/crown.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/processor/crown.rs b/src/processor/crown.rs index 984fdf7..674f114 100644 --- a/src/processor/crown.rs +++ b/src/processor/crown.rs @@ -40,12 +40,12 @@ pub fn process_crown<'a, 'info>( // Require the provided proof account is the current top staker if config.top_staker.ne(&proof_info.key) { - return Err(ProgramError::InvalidAccountData); + return Ok(()); } // Compare balances if proof_new.balance.lt(&proof.balance) { - return Err(ProgramError::InvalidAccountData); + return Ok(()); } } From f5c6975b487ae0bf03765577824eb0c570a5ac9f Mon Sep 17 00:00:00 2001 From: Hardhat Chad Date: Thu, 27 Jun 2024 13:17:53 +0000 Subject: [PATCH 05/10] begin testing new staking mechanism --- src/processor/mine.rs | 22 +++++++++++----------- src/processor/stake.rs | 17 ++++++++++++++--- src/state/bus.rs | 1 - src/state/config.rs | 5 ++++- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/processor/mine.rs b/src/processor/mine.rs index a3df1c8..f941747 100644 --- a/src/processor/mine.rs +++ b/src/processor/mine.rs @@ -102,23 +102,23 @@ pub fn process_mine<'a, 'info>( 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 will receive 2x multiplier. + // Any less than this, and they will receive between 1x and 2x. Miners are only eligable for a multipler + // if their 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 let t = clock.unix_timestamp; diff --git a/src/processor/stake.rs b/src/processor/stake.rs index 79be3dd..d988520 100644 --- a/src/processor/stake.rs +++ b/src/processor/stake.rs @@ -4,8 +4,11 @@ use solana_program::{ }; use crate::{ - instruction::StakeArgs, loaders::*, state::Proof, utils::AccountDeserialize, MINT_ADDRESS, - TREASURY_ADDRESS, + instruction::StakeArgs, + loaders::*, + state::{Config, Proof}, + utils::AccountDeserialize, + MINT_ADDRESS, TREASURY_ADDRESS, }; /// Stake deposits Ore into a miner's proof account to earn multiplier. Its responsibilies include: @@ -26,10 +29,13 @@ pub fn process_stake<'a, 'info>( let amount = u64::from_le_bytes(args.amount); // Load accounts - let [signer, proof_info, sender_info, treasury_tokens_info, token_program] = accounts else { + let [signer, config_info, proof_info, sender_info, treasury_tokens_info, token_program] = + accounts + else { return Err(ProgramError::NotEnoughAccountKeys); }; load_signer(signer)?; + load_config(config_info, true)?; load_proof(proof_info, signer.key, true)?; load_token_account(sender_info, Some(signer.key), &MINT_ADDRESS, true)?; load_token_account( @@ -49,6 +55,11 @@ pub fn process_stake<'a, 'info>( let clock = Clock::get().or(Err(ProgramError::InvalidAccountData))?; proof.last_stake_at = clock.unix_timestamp; + // Update the max stake tracker + let mut config_data = config_info.data.borrow_mut(); + let config = Config::try_from_bytes_mut(&mut config_data)?; + config.max_stake = config.max_stake.max(proof.balance); + // Distribute tokens from signer to treasury solana_program::program::invoke( &spl_token::instruction::transfer( 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..b3de137 100644 --- a/src/state/config.rs +++ b/src/state/config.rs @@ -17,8 +17,11 @@ 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 stake account on the network. + pub max_stake: u64, } impl Discriminator for Config { From f1a914683021481c43ee08314382fdea8281863a Mon Sep 17 00:00:00 2001 From: Hardhat Chad Date: Thu, 27 Jun 2024 14:01:52 +0000 Subject: [PATCH 06/10] crown new top staker --- src/consts.rs | 12 ++------ src/instruction.rs | 7 +++++ src/lib.rs | 1 + src/loaders.rs | 28 ++++++++++++++++++ src/processor/crown.rs | 57 +++++++++++++++++++++++++++++++++++++ src/processor/initialize.rs | 2 ++ src/processor/mine.rs | 2 +- src/processor/mod.rs | 2 ++ src/processor/stake.rs | 17 ++--------- src/state/config.rs | 5 +++- 10 files changed, 108 insertions(+), 25 deletions(-) create mode 100644 src/processor/crown.rs 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..40322eb 100644 --- a/src/instruction.rs +++ b/src/instruction.rs @@ -77,6 +77,13 @@ pub enum OreInstruction { #[account(2, name = "proof", desc = "Ore proof account", writable)] Update = 6, + #[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 = 6, + #[account(0, name = "ore_program", desc = "Ore program")] #[account(1, name = "signer", desc = "Signer", signer)] #[account(2, name = "beneficiary", desc = "Beneficiary token account", writable)] 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..5d18ecb --- /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 proof_info.key.ne(&config.top_staker) { + return Err(ProgramError::InvalidAccountData); + } + + // Compare balances + if proof_new.balance.lt(&proof.balance) { + return Err(ProgramError::InvalidAccountData); + } + } + + // 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 f941747..0be1928 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: 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/processor/stake.rs b/src/processor/stake.rs index d988520..79be3dd 100644 --- a/src/processor/stake.rs +++ b/src/processor/stake.rs @@ -4,11 +4,8 @@ use solana_program::{ }; use crate::{ - instruction::StakeArgs, - loaders::*, - state::{Config, Proof}, - utils::AccountDeserialize, - MINT_ADDRESS, TREASURY_ADDRESS, + instruction::StakeArgs, loaders::*, state::Proof, utils::AccountDeserialize, MINT_ADDRESS, + TREASURY_ADDRESS, }; /// Stake deposits Ore into a miner's proof account to earn multiplier. Its responsibilies include: @@ -29,13 +26,10 @@ pub fn process_stake<'a, 'info>( let amount = u64::from_le_bytes(args.amount); // Load accounts - let [signer, config_info, proof_info, sender_info, treasury_tokens_info, token_program] = - accounts - else { + let [signer, proof_info, sender_info, treasury_tokens_info, token_program] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; load_signer(signer)?; - load_config(config_info, true)?; load_proof(proof_info, signer.key, true)?; load_token_account(sender_info, Some(signer.key), &MINT_ADDRESS, true)?; load_token_account( @@ -55,11 +49,6 @@ pub fn process_stake<'a, 'info>( let clock = Clock::get().or(Err(ProgramError::InvalidAccountData))?; proof.last_stake_at = clock.unix_timestamp; - // Update the max stake tracker - let mut config_data = config_info.data.borrow_mut(); - let config = Config::try_from_bytes_mut(&mut config_data)?; - config.max_stake = config.max_stake.max(proof.balance); - // Distribute tokens from signer to treasury solana_program::program::invoke( &spl_token::instruction::transfer( diff --git a/src/state/config.rs b/src/state/config.rs index b3de137..2c8214f 100644 --- a/src/state/config.rs +++ b/src/state/config.rs @@ -20,8 +20,11 @@ pub struct Config { /// The timestamp of the last reset. pub last_reset_at: i64, - /// The largest stake account on the network. + /// 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 { From 8730052e4c134c8dde1dd12e6191f6123c830b11 Mon Sep 17 00:00:00 2001 From: Hardhat Chad Date: Thu, 27 Jun 2024 14:03:02 +0000 Subject: [PATCH 07/10] if --- src/processor/crown.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/processor/crown.rs b/src/processor/crown.rs index 5d18ecb..984fdf7 100644 --- a/src/processor/crown.rs +++ b/src/processor/crown.rs @@ -39,7 +39,7 @@ pub fn process_crown<'a, 'info>( let proof = Proof::try_from_bytes(&proof_data)?; // Require the provided proof account is the current top staker - if proof_info.key.ne(&config.top_staker) { + if config.top_staker.ne(&proof_info.key) { return Err(ProgramError::InvalidAccountData); } From 2f60bf1f81144295ba29aeedab49b7a03e126f50 Mon Sep 17 00:00:00 2001 From: Hardhat Chad Date: Thu, 27 Jun 2024 14:46:14 +0000 Subject: [PATCH 08/10] silent error --- src/processor/crown.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/processor/crown.rs b/src/processor/crown.rs index 984fdf7..674f114 100644 --- a/src/processor/crown.rs +++ b/src/processor/crown.rs @@ -40,12 +40,12 @@ pub fn process_crown<'a, 'info>( // Require the provided proof account is the current top staker if config.top_staker.ne(&proof_info.key) { - return Err(ProgramError::InvalidAccountData); + return Ok(()); } // Compare balances if proof_new.balance.lt(&proof.balance) { - return Err(ProgramError::InvalidAccountData); + return Ok(()); } } From d718360cafbd9d931b16bdbd795efa7488eb53f6 Mon Sep 17 00:00:00 2001 From: Hardhat Chad Date: Fri, 28 Jun 2024 13:52:38 +0000 Subject: [PATCH 09/10] fix instruction counter --- src/instruction.rs | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/src/instruction.rs b/src/instruction.rs index addb739..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,26 +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, - - #[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 = 6, - - #[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 = 6, + Update = 7, #[account(0, name = "ore_program", desc = "Ore program")] #[account(1, name = "signer", desc = "Signer", signer)] @@ -99,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)] From 3acb7a03be3b13f1969569c79eacb4c4df512f86 Mon Sep 17 00:00:00 2001 From: Hardhat Chad Date: Fri, 28 Jun 2024 14:15:13 +0000 Subject: [PATCH 10/10] cleanup --- src/processor/mine.rs | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/src/processor/mine.rs b/src/processor/mine.rs index 0be1928..e66b8aa 100644 --- a/src/processor/mine.rs +++ b/src/processor/mine.rs @@ -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,17 +94,16 @@ 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. - // If user has greater than or equal to the max stake on the network, they will receive 2x multiplier. - // Any less than this, and they will receive between 1x and 2x. Miners are only eligable for a multipler - // if their last stake deposit was more than one minute ago. + // 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 @@ -117,19 +116,17 @@ pub fn process_mine<'a, 'info>( .saturating_mul(reward) .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), } }