diff --git a/api/src/consts.rs b/api/src/consts.rs index 0235ffc..586465f 100644 --- a/api/src/consts.rs +++ b/api/src/consts.rs @@ -20,6 +20,9 @@ pub const INTERMISSION_SLOTS: u64 = 35; /// The maximum token supply (5 million). pub const MAX_SUPPLY: u64 = ONE_ORE * 5_000_000; +/// The seed of the automation account PDA. +pub const AUTOMATION: &[u8] = b"automation"; + /// The seed of the board account PDA. pub const BOARD: &[u8] = b"board"; diff --git a/api/src/instruction.rs b/api/src/instruction.rs index 113cd5e..1232429 100644 --- a/api/src/instruction.rs +++ b/api/src/instruction.rs @@ -4,24 +4,34 @@ use steel::*; #[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] pub enum OreInstruction { // User - Boost = 0, - ClaimSOL = 1, - ClaimORE = 2, - Deploy = 3, - Initialize = 4, - Log = 5, + Automate = 0, + Boost = 1, + ClaimSOL = 2, + ClaimORE = 3, + Deploy = 4, + Initialize = 5, + Log = 6, Reset = 7, - SetExecutor = 9, // Admin - Bury = 10, - SetAdmin = 11, - SetFeeCollector = 12, + Bury = 9, + SetAdmin = 10, + SetFeeCollector = 11, // Seeker ClaimSeeker = 14, } +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct Automate { + pub amount: [u8; 8], + pub deposit: [u8; 8], + pub fee: [u8; 8], + pub mask: [u8; 8], + pub strategy: u8, +} + #[repr(C)] #[derive(Clone, Copy, Debug, Pod, Zeroable)] pub struct Boost {} @@ -78,12 +88,6 @@ pub struct Uncommit { pub amount: [u8; 8], } -#[repr(C)] -#[derive(Clone, Copy, Debug, Pod, Zeroable)] -pub struct SetExecutor { - pub executor: [u8; 32], -} - #[repr(C)] #[derive(Clone, Copy, Debug, Pod, Zeroable)] pub struct SetAdmin { @@ -112,6 +116,7 @@ pub struct Bury { #[derive(Clone, Copy, Debug, Pod, Zeroable)] pub struct ClaimSeeker {} +instruction!(OreInstruction, Automate); instruction!(OreInstruction, Boost); instruction!(OreInstruction, ClaimSOL); instruction!(OreInstruction, ClaimORE); @@ -120,7 +125,6 @@ instruction!(OreInstruction, Initialize); instruction!(OreInstruction, Log); instruction!(OreInstruction, Bury); instruction!(OreInstruction, Reset); -instruction!(OreInstruction, SetExecutor); instruction!(OreInstruction, SetAdmin); instruction!(OreInstruction, SetFeeCollector); instruction!(OreInstruction, ClaimSeeker); diff --git a/api/src/sdk.rs b/api/src/sdk.rs index 13253db..11eaac6 100644 --- a/api/src/sdk.rs +++ b/api/src/sdk.rs @@ -21,6 +21,39 @@ pub fn program_log(accounts: &[AccountInfo], msg: &[u8]) -> Result<(), ProgramEr invoke_signed(&log(*accounts[0].key, msg), accounts, &crate::ID, &[BOARD]) } +// let [signer_info, automation_info, executor_info, miner_info, system_program] = accounts else { + +pub fn automate( + signer: Pubkey, + amount: u64, + deposit: u64, + executor: Pubkey, + fee: u64, + mask: u64, + strategy: u8, +) -> Instruction { + let automation_address = automation_pda(signer).0; + let miner_address = miner_pda(signer).0; + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(signer, true), + AccountMeta::new(automation_address, false), + AccountMeta::new(executor, false), + AccountMeta::new(miner_address, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: Automate { + amount: amount.to_le_bytes(), + deposit: deposit.to_le_bytes(), + fee: fee.to_le_bytes(), + mask: mask.to_le_bytes(), + strategy: strategy as u8, + } + .to_bytes(), + } +} + // let [signer_info, config_info, mint_info, reserve_tokens_info, treasury_info, system_program, token_program] = pub fn boost(signer: Pubkey) -> Instruction { @@ -115,13 +148,15 @@ pub fn claim_ore(signer: Pubkey, amount: u64) -> Instruction { pub fn deploy( signer: Pubkey, + authority: Pubkey, fee_collector: Pubkey, amount: u64, squares: [bool; 25], ) -> Instruction { + let automation_address = automation_pda(authority).0; let board_address = board_pda().0; let config_address = config_pda().0; - let miner_address = miner_pda(signer).0; + let miner_address = miner_pda(authority).0; let square_address = square_pda().0; // Convert array of 25 booleans into a 32-bit mask where each bit represents whether @@ -137,6 +172,7 @@ pub fn deploy( program_id: crate::ID, accounts: vec![ AccountMeta::new(signer, true), + AccountMeta::new(automation_address, false), AccountMeta::new(board_address, false), AccountMeta::new(config_address, false), AccountMeta::new(fee_collector, false), diff --git a/api/src/state/automation.rs b/api/src/state/automation.rs new file mode 100644 index 0000000..9d27536 --- /dev/null +++ b/api/src/state/automation.rs @@ -0,0 +1,52 @@ +use steel::*; + +use crate::state::miner_pda; + +use super::OreAccount; + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +pub struct Automation { + /// The amount of SOL to deploy on each territory per round. + pub amount: u64, + + /// The authority of this automation account. + pub authority: Pubkey, + + /// The amount of SOL this automation has left. + pub balance: u64, + + /// The executor of this automation account. + pub executor: Pubkey, + + /// The amount of SOL the executor should receive in fees. + pub fee: u64, + + /// The strategy this automation uses. + pub strategy: u64, + + /// The mask of squares this automation should deploy to if preferred strategy. + /// If strategy is Random, first bit is used to determine how many squares to deploy to. + pub mask: u64, +} + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)] +pub enum AutomationStrategy { + Random = 0, + Preferred = 1, +} + +impl AutomationStrategy { + pub fn from_u64(value: u64) -> Self { + Self::try_from(value as u8).unwrap() + } +} + +impl Automation { + pub fn pda(&self) -> (Pubkey, u8) { + miner_pda(self.authority) + } +} + +account!(OreAccount, Automation); diff --git a/api/src/state/miner.rs b/api/src/state/miner.rs index a863ca8..c8b3da4 100644 --- a/api/src/state/miner.rs +++ b/api/src/state/miner.rs @@ -14,6 +14,7 @@ pub struct Miner { pub deployed: [u64; 25], /// The executor with permmission to deploy capital with this account. + #[deprecated(note = "Use automation executor instead")] pub executor: Pubkey, /// The amount of SOL this miner can claim. diff --git a/api/src/state/mod.rs b/api/src/state/mod.rs index e142652..63cc36b 100644 --- a/api/src/state/mod.rs +++ b/api/src/state/mod.rs @@ -1,9 +1,11 @@ +mod automation; mod board; mod config; mod miner; mod square; mod treasury; +pub use automation::*; pub use board::*; pub use config::*; pub use miner::*; @@ -17,6 +19,7 @@ use steel::*; #[repr(u8)] #[derive(Clone, Copy, Debug, Eq, PartialEq, IntoPrimitive, TryFromPrimitive)] pub enum OreAccount { + Automation = 100, Config = 101, Miner = 103, Treasury = 104, @@ -26,6 +29,10 @@ pub enum OreAccount { Square = 106, } +pub fn automation_pda(authority: Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address(&[AUTOMATION, &authority.to_bytes()], &crate::ID) +} + pub fn board_pda() -> (Pubkey, u8) { Pubkey::find_program_address(&[BOARD], &crate::ID) } diff --git a/cli/src/main.rs b/cli/src/main.rs index 6c691a0..b91421e 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -205,7 +205,13 @@ async fn deploy( let mut squares = [false; 25]; squares[square_id as usize] = true; - let ix = ore_api::sdk::deploy(payer.pubkey(), config.fee_collector, amount, squares); + let ix = ore_api::sdk::deploy( + payer.pubkey(), + payer.pubkey(), + config.fee_collector, + amount, + squares, + ); submit_transaction(rpc, payer, &[ix]).await?; Ok(()) } @@ -218,7 +224,13 @@ async fn deploy_all( let amount = u64::from_str(&amount).expect("Invalid AMOUNT"); let config = get_config(rpc).await?; let squares = [true; 25]; - let ix = ore_api::sdk::deploy(payer.pubkey(), config.fee_collector, amount, squares); + let ix = ore_api::sdk::deploy( + payer.pubkey(), + payer.pubkey(), + config.fee_collector, + amount, + squares, + ); submit_transaction(rpc, payer, &[ix]).await?; Ok(()) } diff --git a/program/src/automate.rs b/program/src/automate.rs new file mode 100644 index 0000000..587835f --- /dev/null +++ b/program/src/automate.rs @@ -0,0 +1,81 @@ +use ore_api::prelude::*; +use steel::*; + +use crate::whitelist::AUTHORIZED_ACCOUNTS; + +/// Sets the executor. +pub fn process_automate(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { + // Parse data. + let args = Automate::try_from_bytes(data)?; + let amount = u64::from_le_bytes(args.amount); + let deposit = u64::from_le_bytes(args.deposit); + let fee = u64::from_le_bytes(args.fee); + let mask = u64::from_le_bytes(args.mask); + let strategy = AutomationStrategy::from_u64(args.strategy as u64); + + // Load accounts. + let [signer_info, automation_info, executor_info, miner_info, system_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + signer_info.is_signer()?; + automation_info.is_writable()?; + miner_info + .as_account_mut::(&ore_api::ID)? + .assert_mut_err( + |m| m.authority == *signer_info.key, + OreError::NotAuthorized.into(), + )?; + system_program.is_program(&system_program::ID)?; + + // Check whitelist + if !AUTHORIZED_ACCOUNTS.contains(&signer_info.key) { + return Err(trace("Not authorized", OreError::NotAuthorized.into())); + } + + // Close account if executor is Pubkey::default(). + if *executor_info.key == Pubkey::default() { + automation_info + .as_account_mut::(&ore_api::ID)? + .assert_mut_err( + |a| a.authority == *signer_info.key, + OreError::NotAuthorized.into(), + )?; + automation_info.close(signer_info)?; + return Ok(()); + } + + // Create automation. + let automation = if automation_info.data_is_empty() { + create_program_account::( + automation_info, + system_program, + signer_info, + &ore_api::ID, + &[AUTOMATION, &signer_info.key.to_bytes()], + )?; + let automation = automation_info.as_account_mut::(&ore_api::ID)?; + automation.balance = 0; + automation.authority = *signer_info.key; + automation + } else { + automation_info + .as_account_mut::(&ore_api::ID)? + .assert_mut_err( + |a| a.authority == *signer_info.key, + OreError::NotAuthorized.into(), + )? + }; + + // Set strategy and mask. + automation.amount = amount; + automation.balance += deposit; + automation.executor = *executor_info.key; + automation.fee = fee; + automation.mask = mask; + automation.strategy = strategy as u64; + + // Transfer balance to executor. + automation_info.collect(deposit, signer_info)?; + + Ok(()) +} diff --git a/program/src/deploy.rs b/program/src/deploy.rs index 4e9208b..34acb4a 100644 --- a/program/src/deploy.rs +++ b/program/src/deploy.rs @@ -1,5 +1,5 @@ use ore_api::prelude::*; -use solana_program::{log::sol_log, native_token::lamports_to_sol, rent::Rent}; +use solana_program::{keccak::hashv, log::sol_log, native_token::lamports_to_sol}; use steel::*; use crate::whitelist::AUTHORIZED_ACCOUNTS; @@ -8,33 +8,18 @@ use crate::whitelist::AUTHORIZED_ACCOUNTS; pub fn process_deploy(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { // Parse data. let args = Deploy::try_from_bytes(data)?; - let amount = u64::from_le_bytes(args.amount); + let mut amount = u64::from_le_bytes(args.amount); let mask = u32::from_le_bytes(args.squares); - // Convert 32-bit mask into array of 25 booleans, where each bit in the mask - // determines if that square index is selected (true) or not (false) - let mut squares = [false; 25]; - for i in 0..25 { - squares[i] = (mask & (1 << i)) != 0; - } - - sol_log( - &format!( - "Deploying {} SOL to {} squares", - lamports_to_sol(amount), - squares.iter().filter(|&&s| s).count(), - ) - .as_str(), - ); - // Load accounts. let clock = Clock::get()?; - let [signer_info, board_info, config_info, fee_collector_info, miner_info, square_info, system_program] = + let [signer_info, automation_info, board_info, config_info, fee_collector_info, miner_info, square_info, system_program] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; signer_info.is_signer()?; + automation_info.is_writable()?; let board = board_info .as_account_mut::(&ore_api::ID)? .assert_mut(|b| { @@ -54,11 +39,60 @@ pub fn process_deploy(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResul return Err(trace("Not authorized", OreError::NotAuthorized.into())); } + // Check if signer is the automation executor. + let automation = if !automation_info.data_is_empty() { + let automation = automation_info + .as_account_mut::(&ore_api::ID)? + .assert_mut(|a| a.executor == *signer_info.key)?; + Some(automation) + } else { + None + }; + + // Update amount and mask for automation. + let mut squares = [false; 25]; + if let Some(automation) = &automation { + // Set amount + amount = automation.amount; + + // Set squares + match AutomationStrategy::from_u64(automation.strategy as u64) { + AutomationStrategy::Preferred => { + // Preferred automation strategy. Use the miner authority's provided mask. + for i in 0..25 { + squares[i] = (automation.mask & (1 << i)) != 0; + } + } + AutomationStrategy::Random => { + // Random automation strategy. Generate a random mask based on number of squares user wants to deploy to. + let num_squares = ((automation.mask & 0xFF) as u64).min(25); + let r = hashv(&[&automation.authority.to_bytes(), &board.id.to_le_bytes()]).0; + squares = generate_random_mask(num_squares, &r); + } + } + } else { + // Convert provided 32-bit mask into array of 25 booleans, where each bit in the mask + // determines if that square index is selected (true) or not (false) + for i in 0..25 { + squares[i] = (mask & (1 << i)) != 0; + } + } + // Check minimum amount. if amount < config.min_deploy_amount { return Err(trace("Amount too small", OreError::AmountTooSmall.into())); } + // Log + sol_log( + &format!( + "Deploying {} SOL to {} squares", + lamports_to_sol(amount), + squares.iter().filter(|&&s| s).count(), + ) + .as_str(), + ); + // Create miner. let miner = if miner_info.data_is_empty() { create_program_account::( @@ -80,7 +114,14 @@ pub fn process_deploy(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResul } else { miner_info .as_account_mut::(&ore_api::ID)? - .assert_mut(|m| m.authority == *signer_info.key || m.executor == *signer_info.key)? + .assert_mut(|m| { + if let Some(automation) = &automation { + // only run automation once per round + m.authority == automation.authority && m.round_id < board.id + } else { + m.authority == *signer_info.key + } + })? }; // Reset board. @@ -111,7 +152,7 @@ pub fn process_deploy(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResul let fee = amount / 100; let amount = amount - fee; - // Make all deployments. + // Calculate all deployments. let mut total_fee = 0; let mut total_amount = 0; for (square_id, &should_deploy) in squares.iter().enumerate() { @@ -138,25 +179,33 @@ pub fn process_deploy(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResul } } - // Check total amount. - if *signer_info.key == miner.executor { - let account_size = 8 + std::mem::size_of::(); - let min_rent = Rent::get()?.minimum_balance(account_size); - let claimable_sol = miner.rewards_sol; - let obligations = min_rent + claimable_sol; - let total_transfer = total_amount + total_fee; - let new_lamports = miner_info.lamports().saturating_sub(total_transfer); - if new_lamports < obligations { - return Err(trace( - "Miner account has insufficient SOL", - ProgramError::InsufficientFunds, - )); - } + // Transfer SOL. + if let Some(automation) = automation { + automation.balance -= total_amount + total_fee + automation.fee; + automation_info.send(total_amount, &board_info); + automation_info.send(total_fee, &fee_collector_info); + automation_info.send(automation.fee, &signer_info); + } else { + board_info.collect(total_amount, &signer_info)?; + fee_collector_info.collect(total_fee, &signer_info)?; } - // Transfer deployed. - board_info.collect(total_amount, &signer_info)?; - fee_collector_info.collect(total_fee, &signer_info)?; - Ok(()) } + +fn generate_random_mask(num_squares: u64, r: &[u8]) -> [bool; 25] { + let mut new_mask = [false; 25]; + let mut selected = 0; + for i in 0..25 { + let rand_byte = r[i]; + let remaining_needed = num_squares as u64 - selected as u64; + let remaining_positions = 25 - i; + if remaining_needed > 0 + && (rand_byte as u64) * (remaining_positions as u64) < (remaining_needed * 256) + { + new_mask[i] = true; + selected += 1; + } + } + new_mask +} diff --git a/program/src/lib.rs b/program/src/lib.rs index c2c4a96..6db98b1 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -1,5 +1,6 @@ mod boost; // mod bury; +mod automate; mod claim_ore; mod claim_seeker; mod claim_sol; @@ -8,12 +9,12 @@ mod initialize; mod log; mod reset; mod set_admin; -mod set_executor; mod set_fee_collector; mod whitelist; use boost::*; // use bury::*; +use automate::*; use claim_ore::*; use claim_seeker::*; use claim_sol::*; @@ -22,7 +23,6 @@ use initialize::*; use log::*; use reset::*; use set_admin::*; -use set_executor::*; use set_fee_collector::*; use ore_api::instruction::*; @@ -37,6 +37,7 @@ pub fn process_instruction( match ix { // User + OreInstruction::Automate => process_automate(accounts, data)?, OreInstruction::Boost => process_boost(accounts, data)?, OreInstruction::ClaimSOL => process_claim_sol(accounts, data)?, OreInstruction::ClaimORE => process_claim_ore(accounts, data)?, @@ -44,7 +45,6 @@ pub fn process_instruction( OreInstruction::Log => process_log(accounts, data)?, OreInstruction::Initialize => process_initialize(accounts, data)?, OreInstruction::Reset => process_reset(accounts, data)?, - OreInstruction::SetExecutor => process_set_executor(accounts, data)?, // Admin OreInstruction::SetAdmin => process_set_admin(accounts, data)?, diff --git a/program/src/set_executor.rs b/program/src/set_executor.rs deleted file mode 100644 index 21ad808..0000000 --- a/program/src/set_executor.rs +++ /dev/null @@ -1,27 +0,0 @@ -use ore_api::prelude::*; -use steel::*; - -/// Sets the executor. -pub fn process_set_executor(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { - // Parse data. - let args = SetExecutor::try_from_bytes(data)?; - let new_executor = Pubkey::new_from_array(args.executor); - - // Load accounts. - let [signer_info, miner_info, system_program] = accounts else { - return Err(ProgramError::NotEnoughAccountKeys); - }; - signer_info.is_signer()?; - let miner = miner_info - .as_account_mut::(&ore_api::ID)? - .assert_mut_err( - |m| m.authority == *signer_info.key, - OreError::NotAuthorized.into(), - )?; - system_program.is_program(&system_program::ID)?; - - // Set executor. - miner.executor = new_executor; - - Ok(()) -}