From 77bf1951395fa9bbf0167d0174a4e96ba7ec7a9b Mon Sep 17 00:00:00 2001 From: Hardhat Chad Date: Thu, 27 Jun 2024 13:17:53 +0000 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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(()); } }