diff --git a/api/src/consts.rs b/api/src/consts.rs index a19b642..0704c6c 100644 --- a/api/src/consts.rs +++ b/api/src/consts.rs @@ -14,8 +14,26 @@ 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 hour, in seconds. +pub const ONE_HOUR: i64 = 60 * ONE_MINUTE; + /// The duration of one day, in seconds. -pub const ONE_DAY: i64 = 24 * 60 * 60; +pub const ONE_DAY: i64 = 24 * ONE_HOUR; + +/// The number of seconds for when the winning square expires. +pub const ONE_WEEK: i64 = 7 * ONE_DAY; + +/// The number of slots in one week. +pub const ONE_MINUTE_SLOTS: u64 = 150; + +/// The number of slots in one hour. +pub const ONE_HOUR_SLOTS: u64 = 60 * ONE_MINUTE_SLOTS; + +/// The number of slots in one day. +pub const ONE_DAY_SLOTS: u64 = 24 * ONE_HOUR_SLOTS; + +/// The number of slots in one week. +pub const ONE_WEEK_SLOTS: u64 = 7 * ONE_DAY_SLOTS; /// The number of slots for breather between rounds. pub const INTERMISSION_SLOTS: u64 = 35; diff --git a/api/src/instruction.rs b/api/src/instruction.rs index 9d05a6f..b075952 100644 --- a/api/src/instruction.rs +++ b/api/src/instruction.rs @@ -9,24 +9,25 @@ pub enum OreInstruction { Checkpoint = 2, ClaimSOL = 3, ClaimORE = 4, - Deploy = 5, - Initialize = 6, - Log = 7, - Reset = 8, + Close = 5, + Deploy = 6, + Initialize = 7, + Log = 8, + Reset = 9, // Staker - Deposit = 9, - Withdraw = 10, - ClaimYield = 11, + Deposit = 10, + Withdraw = 11, + ClaimYield = 12, // Admin - Bury = 12, - Wrap = 13, - SetAdmin = 14, - SetFeeCollector = 15, + Bury = 13, + Wrap = 14, + SetAdmin = 15, + SetFeeCollector = 16, // Seeker - ClaimSeeker = 16, + ClaimSeeker = 17, } #[repr(C)] @@ -149,8 +150,13 @@ pub struct ClaimSeeker {} #[derive(Clone, Copy, Debug, Pod, Zeroable)] pub struct Checkpoint {} +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct Close {} + instruction!(OreInstruction, Automate); instruction!(OreInstruction, Boost); +instruction!(OreInstruction, Close); instruction!(OreInstruction, Checkpoint); instruction!(OreInstruction, ClaimSOL); instruction!(OreInstruction, ClaimORE); diff --git a/api/src/state/board.rs b/api/src/state/board.rs index a02a26b..7bf7f4f 100644 --- a/api/src/state/board.rs +++ b/api/src/state/board.rs @@ -10,9 +10,6 @@ pub struct Board { /// The current round number. pub round_id: u64, - /// The timestamp at which the current round starts mining. - pub start_at: i64, - /// The slot at which the current round starts mining. pub start_slot: u64, @@ -20,82 +17,6 @@ pub struct Board { pub end_slot: u64, } -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -pub struct Round { - /// The round number. - pub id: u64, - - /// The amount of SOL deployed in each square. - pub deployed: [u64; 25], - - /// The hash of the end slot, provided by solana, used for random number generation. - pub slot_hash: [u8; 32], - - /// The slot at which claims for this round account end. - pub expires_at: i64, - - /// The amount of ORE in the motherlode. - pub motherlode: u64, - - /// The account to which rent should be returned when this account is closed. - pub rent_payer: Pubkey, - - /// The top miner of the round. - pub top_miner: Pubkey, - - /// The amount of ORE to distribute to the top miner. - pub top_miner_reward: u64, - - /// The total amount of SOL deployed in the round. - pub total_deployed: u64, - - /// The total amount of SOL put in the ORE vault. - pub total_vaulted: u64, - - /// The total amount of SOL won by miners for the round. - pub total_winnings: u64, -} - -impl Round { - pub fn rng(&self) -> u64 { - if self.slot_hash == [0; 32] { - return 0; - } - let r1 = u64::from_le_bytes(self.slot_hash[0..8].try_into().unwrap()); - let r2 = u64::from_le_bytes(self.slot_hash[8..16].try_into().unwrap()); - let r3 = u64::from_le_bytes(self.slot_hash[16..24].try_into().unwrap()); - let r4 = u64::from_le_bytes(self.slot_hash[24..32].try_into().unwrap()); - let r = r1 ^ r2 ^ r3 ^ r4; - r - } - - pub fn winning_square(&self, rng: u64) -> usize { - (rng % 25) as usize - } - - pub fn top_miner_sample(&self, rng: u64, winning_square: usize) -> u64 { - if self.deployed[winning_square] == 0 { - return 0; - } - rng.reverse_bits() % self.deployed[winning_square] - } - - pub fn calculate_total_winnings(&self, winning_square: usize) -> u64 { - let mut total_winnings = 0; - for (i, &deployed) in self.deployed.iter().enumerate() { - if i != winning_square { - total_winnings += deployed; - } - } - total_winnings - } - - pub fn did_hit_motherlode(&self, rng: u64) -> bool { - rng.reverse_bits() % 625 == 0 - } -} - impl Board { pub fn pda(&self) -> (Pubkey, u8) { board_pda() @@ -103,20 +24,3 @@ impl Board { } account!(OreAccount, Board); - -account!(OreAccount, Round); - -#[cfg(test)] -mod tests { - use solana_program::rent::Rent; - - use super::*; - - #[test] - fn test_rent() { - let size_of_round = 8 + std::mem::size_of::(); - let required_rent = Rent::default().minimum_balance(size_of_round); - println!("required_rent: {}", required_rent); - assert!(false); - } -} diff --git a/api/src/state/mod.rs b/api/src/state/mod.rs index 364cb24..d8326c3 100644 --- a/api/src/state/mod.rs +++ b/api/src/state/mod.rs @@ -2,8 +2,8 @@ mod automation; mod board; mod config; mod miner; +mod round; mod seeker; -// mod square; mod stake; mod treasury; @@ -11,8 +11,8 @@ pub use automation::*; pub use board::*; pub use config::*; pub use miner::*; +pub use round::*; pub use seeker::*; -// pub use square::*; pub use stake::*; pub use treasury::*; @@ -30,7 +30,6 @@ pub enum OreAccount { // Board = 105, - // Square = 106, Seeker = 107, Stake = 108, Round = 109, @@ -56,10 +55,6 @@ pub fn seeker_pda(mint: Pubkey) -> (Pubkey, u8) { Pubkey::find_program_address(&[SEEKER, &mint.to_bytes()], &crate::ID) } -// pub fn square_pda() -> (Pubkey, u8) { -// Pubkey::find_program_address(&[SQUARE], &crate::ID) -// } - pub fn round_pda(id: u64) -> (Pubkey, u8) { Pubkey::find_program_address(&[ROUND, &id.to_le_bytes()], &crate::ID) } diff --git a/api/src/state/round.rs b/api/src/state/round.rs new file mode 100644 index 0000000..05ff69d --- /dev/null +++ b/api/src/state/round.rs @@ -0,0 +1,102 @@ +use steel::*; + +use crate::state::round_pda; + +use super::OreAccount; + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +pub struct Round { + /// The round number. + pub id: u64, + + /// The amount of SOL deployed in each square. + pub deployed: [u64; 25], + + /// The hash of the end slot, provided by solana, used for random number generation. + pub slot_hash: [u8; 32], + + /// The slot at which claims for this round account end. + pub expires_at: u64, + + /// The amount of ORE in the motherlode. + pub motherlode: u64, + + /// The account to which rent should be returned when this account is closed. + pub rent_payer: Pubkey, + + /// The top miner of the round. + pub top_miner: Pubkey, + + /// The amount of ORE to distribute to the top miner. + pub top_miner_reward: u64, + + /// The total amount of SOL deployed in the round. + pub total_deployed: u64, + + /// The total amount of SOL put in the ORE vault. + pub total_vaulted: u64, + + /// The total amount of SOL won by miners for the round. + pub total_winnings: u64, +} + +impl Round { + pub fn pda(&self) -> (Pubkey, u8) { + round_pda(self.id) + } + + pub fn rng(&self) -> u64 { + if self.slot_hash == [0; 32] { + return 0; + } + let r1 = u64::from_le_bytes(self.slot_hash[0..8].try_into().unwrap()); + let r2 = u64::from_le_bytes(self.slot_hash[8..16].try_into().unwrap()); + let r3 = u64::from_le_bytes(self.slot_hash[16..24].try_into().unwrap()); + let r4 = u64::from_le_bytes(self.slot_hash[24..32].try_into().unwrap()); + let r = r1 ^ r2 ^ r3 ^ r4; + r + } + + pub fn winning_square(&self, rng: u64) -> usize { + (rng % 25) as usize + } + + pub fn top_miner_sample(&self, rng: u64, winning_square: usize) -> u64 { + if self.deployed[winning_square] == 0 { + return 0; + } + rng.reverse_bits() % self.deployed[winning_square] + } + + pub fn calculate_total_winnings(&self, winning_square: usize) -> u64 { + let mut total_winnings = 0; + for (i, &deployed) in self.deployed.iter().enumerate() { + if i != winning_square { + total_winnings += deployed; + } + } + total_winnings + } + + pub fn did_hit_motherlode(&self, rng: u64) -> bool { + rng.reverse_bits() % 625 == 0 + } +} + +account!(OreAccount, Round); + +#[cfg(test)] +mod tests { + use solana_program::rent::Rent; + + use super::*; + + #[test] + fn test_rent() { + let size_of_round = 8 + std::mem::size_of::(); + let required_rent = Rent::default().minimum_balance(size_of_round); + println!("required_rent: {}", required_rent); + assert!(false); + } +} diff --git a/api/src/state/square.rs b/api/src/state/square.rs deleted file mode 100644 index 9d44220..0000000 --- a/api/src/state/square.rs +++ /dev/null @@ -1,26 +0,0 @@ -use steel::*; - -use crate::state::square_pda; - -use super::OreAccount; - -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -pub struct Square { - /// The count of miners on each square. - pub count: [u64; 25], - - /// The deployments of all players on each square. - pub deployed: [[u64; 16]; 25], - - /// The miners authorities on each square. - pub miners: [[Pubkey; 16]; 25], -} - -impl Square { - pub fn pda() -> (Pubkey, u8) { - square_pda() - } -} - -account!(OreAccount, Square); diff --git a/program/src/checkpoint.rs b/program/src/checkpoint.rs index da7d980..fe4ae18 100644 --- a/program/src/checkpoint.rs +++ b/program/src/checkpoint.rs @@ -34,7 +34,7 @@ pub fn process_checkpoint(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program system_program.is_program(&system_program::ID)?; // Ensure round is not expired. - if clock.unix_timestamp >= round.expires_at { + if clock.slot >= round.expires_at { // In this case, the miner forfeits any potential rewards and their checkpoint is recorded. miner.checkpoint_id = round.id; return Ok(()); @@ -42,13 +42,14 @@ pub fn process_checkpoint(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program // Calculate bot fee permissions. let mut bot_fee = 0; - if clock.unix_timestamp > round.expires_at - ONE_DAY { - // We are in the last day before the round expires. - // Anyone is allowed to checkpoint and may collect the bot fee. + if clock.slot >= round.expires_at - ONE_DAY_SLOTS { + // The round expires in less than 24h. + // Anyone is allowed to checkpoint this account and may collect the bot fee. bot_fee = miner.checkpoint_fee; miner.checkpoint_fee = 0; } else { - // There is still time before the round expires. Bots may not yet checkpoint this account. + // There is still time remaining before the round expires. + // Bots may not yet checkpoint this account. automation_info.has_seeds(&[AUTOMATION, &miner.authority.to_bytes()], &ore_api::ID)?; if !automation_info.data_is_empty() { let automation = automation_info diff --git a/program/src/close.rs b/program/src/close.rs new file mode 100644 index 0000000..d62a31b --- /dev/null +++ b/program/src/close.rs @@ -0,0 +1,23 @@ +use ore_api::prelude::*; +use steel::*; + +/// Closes a round accound, and returns the rent to the rent payer. +pub fn process_close(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { + // Load accounts. + let clock = Clock::get()?; + let [signer_info, rent_payer_info, round_info, system_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + signer_info.is_signer()?; + rent_payer_info.is_writable()?; + round_info + .as_account_mut::(&ore_api::ID)? + .assert_mut(|r| r.expires_at >= clock.slot)? // Ensure round has ended. + .assert_mut(|r| r.rent_payer == *rent_payer_info.key)?; // Ensure the rent payer is the correct one. + system_program.is_program(&system_program::ID)?; + + // Close the account. + round_info.close(rent_payer_info)?; + + Ok(()) +} diff --git a/program/src/deploy.rs b/program/src/deploy.rs index 612f26e..ddf5122 100644 --- a/program/src/deploy.rs +++ b/program/src/deploy.rs @@ -11,7 +11,7 @@ pub fn process_deploy(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResul // Load accounts. let clock = Clock::get()?; - let [signer_info, authority_info, automation_info, board_info, miner_info, round_info, round_prev_info, system_program] = + let [signer_info, authority_info, automation_info, board_info, miner_info, round_info, system_program] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); diff --git a/program/src/lib.rs b/program/src/lib.rs index 29d216e..78fe999 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -6,6 +6,7 @@ mod claim_ore; mod claim_seeker; mod claim_sol; mod claim_yield; +mod close; mod deploy; mod deposit; // mod initialize; @@ -25,6 +26,7 @@ use claim_ore::*; use claim_seeker::*; use claim_sol::*; use claim_yield::*; +use close::*; use deploy::*; use deposit::*; // use initialize::*; @@ -55,6 +57,7 @@ pub fn process_instruction( OreInstruction::ClaimORE => process_claim_ore(accounts, data)?, OreInstruction::Deploy => process_deploy(accounts, data)?, OreInstruction::Log => process_log(accounts, data)?, + OreInstruction::Close => process_close(accounts, data)?, // OreInstruction::Initialize => process_initialize(accounts, data)?, OreInstruction::Reset => process_reset(accounts, data)?, diff --git a/program/src/reset.rs b/program/src/reset.rs index 4fc847a..bdd797d 100644 --- a/program/src/reset.rs +++ b/program/src/reset.rs @@ -1,5 +1,5 @@ use ore_api::prelude::*; -use solana_program::{log::sol_log, slot_hashes::SlotHashes}; +use solana_program::slot_hashes::SlotHashes; use steel::*; /// Pays out the winners and block reward. @@ -14,7 +14,7 @@ pub fn process_reset(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResul signer_info.is_signer()?; let board = board_info .as_account_mut::(&ore_api::ID)? - .assert_mut(|b| clock.slot > b.end_slot)?; + .assert_mut(|b| clock.slot >= b.end_slot + INTERMISSION_SLOTS)?; let config = config_info.as_account::(&ore_api::ID)?; fee_collector_info .is_writable()? @@ -160,8 +160,8 @@ pub fn process_reset(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResul start_slot: board.start_slot, end_slot: board.end_slot, winning_square: winning_square as u64, - top_miner: Pubkey::default(), // board.top_miner, - num_winners: 0, // square.count[winning_square], + top_miner: Pubkey::default(), // Unknown + num_winners: 0, // Unknown total_deployed: round.total_deployed, total_vaulted: round.total_vaulted, total_winnings: round.total_winnings, @@ -171,6 +171,32 @@ pub fn process_reset(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResul .to_bytes(), )?; + // Reset board. + board.round_id += 1; + board.start_slot = clock.slot + 1; + board.end_slot = board.start_slot + 150; + + // Open next round account. + create_program_account::( + round_next_info, + ore_program, + signer_info, + &ore_api::ID, + &[ROUND, &(board.round_id + 1).to_le_bytes()], + )?; + let round_next = round_next_info.as_account_mut::(&ore_api::ID)?; + round_next.id = board.round_id + 1; + round_next.deployed = [0; 25]; + round_next.slot_hash = [0; 32]; + round_next.expires_at = board.end_slot + ONE_WEEK_SLOTS; + round_next.rent_payer = *signer_info.key; + round_next.motherlode = 0; + round_next.top_miner = Pubkey::default(); + round_next.top_miner_reward = round.top_miner_reward; + round_next.total_deployed = 0; + round_next.total_vaulted = 0; + round_next.total_winnings = 0; + // Do SOL transfers. round_info.send(total_admin_fee, &fee_collector_info); round_info.send(vault_amount, &treasury_info);