diff --git a/api/src/consts.rs b/api/src/consts.rs index d781432..87eebb4 100644 --- a/api/src/consts.rs +++ b/api/src/consts.rs @@ -38,6 +38,9 @@ pub const SEEKER: &[u8] = b"seeker"; /// The seed of the square account PDA. pub const SQUARE: &[u8] = b"square"; +/// The seed of the stake account PDA. +pub const STAKE: &[u8] = b"stake"; + /// The seed of the treasury account PDA. pub const TREASURY: &[u8] = b"treasury"; diff --git a/api/src/event.rs b/api/src/event.rs index 3ac6e54..b14d579 100644 --- a/api/src/event.rs +++ b/api/src/event.rs @@ -53,7 +53,10 @@ pub struct BuryEvent { pub disc: u64, /// The amount of ORE buried. - pub ore_amount: u64, + pub ore_buried: u64, + + /// The amount of ORE shared with stakers. + pub ore_shared: u64, /// The amount of SOL swapped. pub sol_amount: u64, diff --git a/api/src/instruction.rs b/api/src/instruction.rs index c2e9089..604f22a 100644 --- a/api/src/instruction.rs +++ b/api/src/instruction.rs @@ -3,7 +3,7 @@ use steel::*; #[repr(u8)] #[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] pub enum OreInstruction { - // User + // Miner Automate = 0, Boost = 1, ClaimSOL = 2, @@ -13,11 +13,16 @@ pub enum OreInstruction { Log = 6, Reset = 7, + // Staker + Deposit = 8, + Withdraw = 9, + ClaimYield = 10, + // Admin - Bury = 9, - Wrap = 10, - SetAdmin = 11, - SetFeeCollector = 12, + Bury = 11, + Wrap = 12, + SetAdmin = 13, + SetFeeCollector = 14, // Seeker ClaimSeeker = 15, @@ -118,6 +123,24 @@ pub struct Bury { pub min_amount_out: [u8; 8], } +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct Deposit { + pub amount: [u8; 8], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct Withdraw { + pub amount: [u8; 8], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct ClaimYield { + pub amount: [u8; 8], +} + #[repr(C)] #[derive(Clone, Copy, Debug, Pod, Zeroable)] pub struct ClaimSeeker {} @@ -138,5 +161,8 @@ instruction!(OreInstruction, Bury); instruction!(OreInstruction, Reset); instruction!(OreInstruction, SetAdmin); instruction!(OreInstruction, SetFeeCollector); +instruction!(OreInstruction, Deposit); +instruction!(OreInstruction, Withdraw); +instruction!(OreInstruction, ClaimYield); instruction!(OreInstruction, ClaimSeeker); instruction!(OreInstruction, MigrateMiner); diff --git a/api/src/state/miner.rs b/api/src/state/miner.rs index 8d504fb..f636d85 100644 --- a/api/src/state/miner.rs +++ b/api/src/state/miner.rs @@ -15,10 +15,7 @@ pub struct Miner { /// Unused buffer. #[deprecated(note = "No longer used")] - pub buffer: [u8; 24], - - /// Whether this miner is associated with a Solana Seeker. - pub is_seeker: u64, + pub buffer: [u8; 32], /// The amount of SOL this miner has had refunded and may claim. pub refund_sol: u64, diff --git a/api/src/state/mod.rs b/api/src/state/mod.rs index 6499465..a885d12 100644 --- a/api/src/state/mod.rs +++ b/api/src/state/mod.rs @@ -4,6 +4,7 @@ mod config; mod miner; mod seeker; mod square; +mod stake; mod treasury; pub use automation::*; @@ -12,6 +13,7 @@ pub use config::*; pub use miner::*; pub use seeker::*; pub use square::*; +pub use stake::*; pub use treasury::*; use crate::consts::*; @@ -30,6 +32,7 @@ pub enum OreAccount { Board = 105, Square = 106, Seeker = 107, + Stake = 108, } pub fn automation_pda(authority: Pubkey) -> (Pubkey, u8) { @@ -56,6 +59,10 @@ pub fn square_pda() -> (Pubkey, u8) { Pubkey::find_program_address(&[SQUARE], &crate::ID) } +pub fn stake_pda(authority: Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address(&[STAKE, &authority.to_bytes()], &crate::ID) +} + pub fn treasury_pda() -> (Pubkey, u8) { Pubkey::find_program_address(&[TREASURY], &crate::ID) } diff --git a/api/src/state/stake.rs b/api/src/state/stake.rs new file mode 100644 index 0000000..bfc6b80 --- /dev/null +++ b/api/src/state/stake.rs @@ -0,0 +1,92 @@ +use steel::*; + +use crate::state::{stake_pda, Treasury}; + +use super::OreAccount; + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +pub struct Stake { + /// The authority of this miner account. + pub authority: Pubkey, + + /// The balance of this stake account. + pub balance: u64, + + /// The timestamp of last claim. + pub last_claim_at: i64, + + /// The timestamp the last time this staker deposited. + pub last_deposit_at: i64, + + /// The timestamp the last time this staker withdrew. + pub last_withdraw_at: i64, + + /// The rewards factor last time rewards were updated on this stake account. + pub rewards_factor: Numeric, + + /// The amount of ORE this staker can claim. + pub rewards: u64, + + /// The total amount of ORE this staker has earned over its lifetime. + pub lifetime_rewards: u64, + + /// Flag indicating whether this staker is associated with a Solana Seeker. + pub is_seeker: u64, +} + +impl Stake { + pub fn pda(&self) -> (Pubkey, u8) { + stake_pda(self.authority) + } + + pub fn claim(&mut self, amount: u64, clock: &Clock, treasury: &Treasury) -> u64 { + self.update_rewards(treasury); + let amount = self.rewards.min(amount); + self.rewards -= amount; + self.last_claim_at = clock.unix_timestamp; + amount + } + + pub fn deposit( + &mut self, + amount: u64, + clock: &Clock, + treasury: &mut Treasury, + sender: &TokenAccount, + ) -> u64 { + self.update_rewards(treasury); + let amount = sender.amount().min(amount); + self.balance += amount; + self.last_deposit_at = clock.unix_timestamp; + treasury.total_staked += amount; + amount + } + + pub fn withdraw(&mut self, amount: u64, clock: &Clock, treasury: &mut Treasury) -> u64 { + self.update_rewards(treasury); + let amount = self.balance.min(amount); + self.balance -= amount; + self.last_withdraw_at = clock.unix_timestamp; + treasury.total_staked -= amount; + amount + } + + fn update_rewards(&mut self, treasury: &Treasury) { + // Accumulate rewards, weighted by stake balance. + if treasury.rewards_factor > self.rewards_factor { + let accumulated_rewards = treasury.rewards_factor - self.rewards_factor; + if accumulated_rewards < Numeric::ZERO { + panic!("Accumulated rewards is negative"); + } + let personal_rewards = accumulated_rewards * Numeric::from_u64(self.balance); + self.rewards += personal_rewards.to_u64(); + self.lifetime_rewards += personal_rewards.to_u64(); + } + + // Update this stake account's last seen rewards factor. + self.rewards_factor = treasury.rewards_factor; + } +} + +account!(OreAccount, Stake); diff --git a/api/src/state/treasury.rs b/api/src/state/treasury.rs index 298a4f3..4b47ef3 100644 --- a/api/src/state/treasury.rs +++ b/api/src/state/treasury.rs @@ -10,8 +10,14 @@ pub struct Treasury { // The amount of SOL collected for buy-bury operations. pub balance: u64, - /// The amount of ORE in the motherlode. + /// The amount of ORE in the motherlode rewards pool. pub motherlode: u64, + + /// The cumulative ORE distributed to stakers, divided by the total stake at the time of distribution. + pub rewards_factor: Numeric, + + /// The current total amount of ORE staked. + pub total_staked: u64, } account!(OreAccount, Treasury); diff --git a/program/src/bury.rs b/program/src/bury.rs index 1b7dac6..6d1cc43 100644 --- a/program/src/bury.rs +++ b/program/src/bury.rs @@ -24,7 +24,7 @@ pub fn process_bury(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult .as_account::(&ore_api::ID)? .assert(|c| c.admin == *signer_info.key)?; mint_info.has_address(&MINT_ADDRESS)?.as_mint()?; - treasury_info.as_account_mut::(&ore_api::ID)?; + let treasury = treasury_info.as_account_mut::(&ore_api::ID)?; let treasury_ore = treasury_ore_info.as_associated_token_account(treasury_info.key, &MINT_ADDRESS)?; treasury_sol_info.as_associated_token_account(treasury_info.key, &SOL_MINT)?; @@ -88,10 +88,18 @@ pub fn process_bury(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult treasury_sol_info.as_associated_token_account(treasury_info.key, &SOL_MINT)?; let post_swap_ore_balance = treasury_ore.amount(); let post_swap_sol_balance = treasury_sol.amount(); + let total_ore = post_swap_ore_balance - pre_swap_ore_balance; assert_eq!(post_swap_sol_balance, 0); + // Share some ORE with stakers. + let mut shared_amount = 0; + if treasury.total_staked > 0 { + shared_amount = 0; // TODO: calculate shared amount + treasury.rewards_factor += Numeric::from_fraction(shared_amount, treasury.total_staked); + } + // Burn ORE. - let burn_amount = post_swap_ore_balance - pre_swap_ore_balance; + let burn_amount = total_ore - shared_amount; burn_signed( treasury_ore_info, mint_info, @@ -115,7 +123,8 @@ pub fn process_bury(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult &[board_info.clone(), ore_program.clone()], BuryEvent { disc: 1, - ore_amount: burn_amount, + ore_buried: burn_amount, + ore_shared: shared_amount, sol_amount: pre_swap_sol_balance, new_circulating_supply: mint.supply(), ts: Clock::get()?.unix_timestamp, diff --git a/program/src/claim_seeker.rs b/program/src/claim_seeker.rs index 9b2c640..376460c 100644 --- a/program/src/claim_seeker.rs +++ b/program/src/claim_seeker.rs @@ -1,6 +1,6 @@ use ore_api::{ - consts::{MINER, SEEKER}, - state::{Miner, Seeker}, + consts::{MINER, SEEKER, STAKE}, + state::{Miner, Seeker, Stake}, }; use solana_program::pubkey; use steel::*; @@ -15,17 +15,17 @@ use spl_token_2022::{ /// Claims a Seeker genesis token for a miner. pub fn process_claim_seeker(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { // Load accounts. - let [signer_info, miner_info, mint_info, seeker_info, token_account_info, system_program] = + let [signer_info, mint_info, seeker_info, stake_info, token_account_info, system_program] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; signer_info.is_signer()?; - miner_info.is_writable()?; mint_info.has_owner(&spl_token_2022::ID)?; seeker_info .is_writable()? .has_seeds(&[SEEKER, &mint_info.key.to_bytes()], &ore_api::ID)?; + stake_info.is_writable()?; token_account_info .as_associated_token_account(signer_info.key, mint_info.key)? .assert(|t| t.amount() == 1)?; @@ -68,35 +68,35 @@ pub fn process_claim_seeker(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Progr let seeker = seeker_info.as_account_mut::(&ore_api::ID)?; seeker.mint = *mint_info.key; - // Open miner account. - let miner = if miner_info.data_is_empty() { - create_program_account::( - miner_info, + // Open stake account. + let stake = if stake_info.data_is_empty() { + create_program_account::( + stake_info, system_program, signer_info, &ore_api::ID, - &[MINER, &signer_info.key.to_bytes()], + &[STAKE, &signer_info.key.to_bytes()], )?; - let miner = miner_info.as_account_mut::(&ore_api::ID)?; - miner.authority = *signer_info.key; - miner.deployed = [0; 25]; - miner.is_seeker = 0; - miner.refund_sol = 0; - miner.rewards_sol = 0; - miner.rewards_ore = 0; - miner.round_id = 0; - miner.lifetime_rewards_sol = 0; - miner.lifetime_rewards_ore = 0; - miner + let stake = stake_info.as_account_mut::(&ore_api::ID)?; + stake.authority = *signer_info.key; + stake.balance = 0; + stake.last_claim_at = 0; + stake.last_deposit_at = 0; + stake.last_withdraw_at = 0; + stake.rewards_factor = Numeric::from_u64(0); + stake.rewards = 0; + stake.lifetime_rewards = 0; + stake.is_seeker = 0; + stake } else { - miner_info - .as_account_mut::(&ore_api::ID)? - .assert_mut(|m| m.authority == *signer_info.key)? - .assert_mut(|m| m.is_seeker == 0)? + stake_info + .as_account_mut::(&ore_api::ID)? + .assert_mut(|s| s.authority == *signer_info.key)? + .assert_mut(|s| s.is_seeker == 0)? }; // Flag the miner as a Seeker. - miner.is_seeker = 1; + stake.is_seeker = 1; Ok(()) } diff --git a/program/src/claim_yield.rs b/program/src/claim_yield.rs new file mode 100644 index 0000000..0ebf805 --- /dev/null +++ b/program/src/claim_yield.rs @@ -0,0 +1,78 @@ +use ore_api::prelude::*; +use solana_program::log::sol_log; +use spl_token::amount_to_ui_amount; +use steel::*; + +use crate::AUTHORIZED_ACCOUNTS; + +/// Claims yield from the staking contract. +pub fn process_claim_yield(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { + // Parse data. + let args = ClaimYield::try_from_bytes(data)?; + let amount = u64::from_le_bytes(args.amount); + + // Load accounts. + let clock = Clock::get()?; + let [signer_info, mint_info, recipient_info, stake_info, treasury_info, treasury_tokens_info, system_program, token_program, associated_token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + signer_info.is_signer()?; + mint_info.has_address(&MINT_ADDRESS)?.as_mint()?; + recipient_info + .is_writable()? + .as_associated_token_account(&signer_info.key, &mint_info.key)?; + let stake = stake_info + .as_account_mut::(&ore_api::ID)? + .assert_mut(|s| s.authority == *signer_info.key)?; + let treasury = treasury_info.as_account_mut::(&ore_api::ID)?; + treasury_tokens_info + .is_writable()? + .as_associated_token_account(&treasury_info.key, &mint_info.key)?; + system_program.is_program(&system_program::ID)?; + token_program.is_program(&spl_token::ID)?; + associated_token_program.is_program(&spl_associated_token_account::ID)?; + + // Check whitelist + if !AUTHORIZED_ACCOUNTS.contains(&signer_info.key) { + return Err(trace("Not authorized", OreError::NotAuthorized.into())); + } + + // Open recipient token account. + if recipient_info.data_is_empty() { + create_associated_token_account( + signer_info, + signer_info, + recipient_info, + mint_info, + system_program, + token_program, + associated_token_program, + )?; + } + + // Claim yield from stake account. + let amount = stake.claim(amount, &clock, treasury); + + // Transfer ORE to recipient. + transfer_signed( + treasury_info, + treasury_tokens_info, + recipient_info, + token_program, + amount, + &[TREASURY], + )?; + + // Log claim. + sol_log( + &format!( + "Claiming {} ORE", + amount_to_ui_amount(amount, TOKEN_DECIMALS) + ) + .as_str(), + ); + + Ok(()) +} diff --git a/program/src/deploy.rs b/program/src/deploy.rs index dcfae99..6a485d9 100644 --- a/program/src/deploy.rs +++ b/program/src/deploy.rs @@ -30,11 +30,6 @@ pub fn process_deploy(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResul let square = square_info.as_account_mut::(&ore_api::ID)?; system_program.is_program(&system_program::ID)?; - // // Check whitelist - // if !AUTHORIZED_ACCOUNTS.contains(&signer_info.key) { - // 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 @@ -98,7 +93,6 @@ pub fn process_deploy(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResul let miner = miner_info.as_account_mut::(&ore_api::ID)?; miner.authority = *signer_info.key; miner.deployed = [0; 25]; - miner.is_seeker = 0; miner.refund_sol = 0; miner.rewards_sol = 0; miner.rewards_ore = 0; diff --git a/program/src/deposit.rs b/program/src/deposit.rs new file mode 100644 index 0000000..1cc8e5e --- /dev/null +++ b/program/src/deposit.rs @@ -0,0 +1,86 @@ +use ore_api::prelude::*; +use solana_program::log::sol_log; +use spl_token::amount_to_ui_amount; +use steel::*; + +use crate::AUTHORIZED_ACCOUNTS; + +/// Deposits ORE into the staking contract. +pub fn process_deposit(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { + // Parse data. + let args = Deposit::try_from_bytes(data)?; + let amount = u64::from_le_bytes(args.amount); + + // Load accounts. + let clock = Clock::get()?; + let [signer_info, sender_info, stake_info, treasury_info, treasury_tokens_info, system_program, token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + signer_info.is_signer()?; + let sender = sender_info + .is_writable()? + .as_associated_token_account(&signer_info.key, &MINT_ADDRESS)?; + stake_info.is_writable()?; + let treasury = treasury_info.as_account_mut::(&ore_api::ID)?; + treasury_tokens_info + .is_writable()? + .as_associated_token_account(&treasury_info.key, &MINT_ADDRESS)?; + system_program.is_program(&system_program::ID)?; + token_program.is_program(&spl_token::ID)?; + + // Check whitelist + if !AUTHORIZED_ACCOUNTS.contains(&signer_info.key) { + return Err(trace("Not authorized", OreError::NotAuthorized.into())); + } + + // Open stake account. + let stake = if stake_info.data_is_empty() { + create_program_account::( + stake_info, + system_program, + &signer_info, + &ore_api::ID, + &[STAKE, &signer_info.key.to_bytes()], + )?; + let stake = stake_info.as_account_mut::(&ore_api::ID)?; + stake.authority = *signer_info.key; + stake.balance = 0; + stake.last_claim_at = 0; + stake.last_deposit_at = 0; + stake.last_withdraw_at = 0; + stake.is_seeker = 0; + stake.rewards_factor = treasury.rewards_factor; + stake.rewards = 0; + stake.lifetime_rewards = 0; + stake + } else { + stake_info + .as_account_mut::(&ore_api::ID)? + .assert_mut(|s| s.authority == *signer_info.key)? + }; + + // Deposit into stake account. + let amount = stake.deposit(amount, &clock, treasury, &sender); + + // Transfer ORE to treasury. + transfer( + signer_info, + sender_info, + treasury_tokens_info, + token_program, + amount, + )?; + + // Log deposit. + sol_log( + &format!( + "Depositing {} ORE", + amount_to_ui_amount(amount, TOKEN_DECIMALS) + ) + .as_str(), + ); + + Ok(()) +} diff --git a/program/src/lib.rs b/program/src/lib.rs index d8e9767..4f1a5a9 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -4,13 +4,17 @@ mod bury; mod claim_ore; mod claim_seeker; mod claim_sol; +mod claim_yield; mod deploy; +mod deposit; mod initialize; mod log; mod migrate_miner; mod reset; mod set_admin; mod set_fee_collector; +mod whitelist; +mod withdraw; mod wrap; use automate::*; @@ -19,13 +23,17 @@ use bury::*; use claim_ore::*; use claim_seeker::*; use claim_sol::*; +use claim_yield::*; use deploy::*; +use deposit::*; use initialize::*; use log::*; use migrate_miner::*; use reset::*; use set_admin::*; use set_fee_collector::*; +use whitelist::*; +use withdraw::*; use wrap::*; use ore_api::instruction::*; @@ -39,7 +47,7 @@ pub fn process_instruction( let (ix, data) = parse_instruction(&ore_api::ID, program_id, data)?; match ix { - // User + // Miner OreInstruction::Automate => process_automate(accounts, data)?, OreInstruction::Boost => process_boost(accounts, data)?, OreInstruction::ClaimSOL => process_claim_sol(accounts, data)?, @@ -49,6 +57,11 @@ pub fn process_instruction( OreInstruction::Initialize => process_initialize(accounts, data)?, OreInstruction::Reset => process_reset(accounts, data)?, + // Staker + OreInstruction::Deposit => process_deposit(accounts, data)?, + OreInstruction::Withdraw => process_withdraw(accounts, data)?, + OreInstruction::ClaimYield => process_claim_yield(accounts, data)?, + // Admin OreInstruction::Bury => process_bury(accounts, data)?, OreInstruction::Wrap => process_wrap(accounts, data)?, @@ -58,6 +71,7 @@ pub fn process_instruction( // Seeker OreInstruction::ClaimSeeker => process_claim_seeker(accounts, data)?, OreInstruction::MigrateMiner => process_migrate_miner(accounts, data)?, + _ => return Err(ProgramError::InvalidInstructionData), } Ok(()) diff --git a/program/src/migrate_miner.rs b/program/src/migrate_miner.rs index 0dc66ff..58e6b72 100644 --- a/program/src/migrate_miner.rs +++ b/program/src/migrate_miner.rs @@ -20,9 +20,7 @@ pub fn process_migrate_miner(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Prog // Set seeker activation flag. config.is_seeker_activation_enabled = 0; - // Set seeker flag. - miner.is_seeker = 0; - miner.buffer = [0; 24]; + // No op Ok(()) } diff --git a/program/src/whitelist.rs b/program/src/whitelist.rs index 875ccde..9a6b2f9 100644 --- a/program/src/whitelist.rs +++ b/program/src/whitelist.rs @@ -1,286 +1,5 @@ use solana_program::pubkey; use steel::*; -#[deprecated(note = "No longer used")] -pub const AUTHORIZED_ACCOUNTS: [Pubkey; 280] = [ - pubkey!("pqspJ298ryBjazPAr95J9sULCVpZe3HbZTWkbC1zrkS"), - pubkey!("HNWhK5f8RMWBqcA7mXJPaxdTPGrha3rrqUrri7HSKb3T"), - pubkey!("6B9PjpHfbhPcSakS5UQ7ZctgbPujfsryVRpDecskGLiz"), - pubkey!("HBUh9g46wk2X89CvaNN15UmsznP59rh6od1h8JwYAopk"), - pubkey!("By5JFFueXCqeqLk5MzR8ZSwFxASz3SKWX2TVfT1LTFbX"), - pubkey!("J89R2jNKbfkFoJjvkjnwwepvJRE2M8VPQ67RhPeQfVY8"), - pubkey!("6Qaf8uCcYWkWb12FZYUhuqkae3np2WiaZCv7ic4PMf72"), - pubkey!("DQLBoeyCkUuGMHmsEBBJ5LMzdXDza89NLEdzkbtjMfXq"), - pubkey!("Gw7kkmtMp4abR4KHjDK3rS5XAQR85GSDHNRniTQR9t2n"), - pubkey!("BiK1WCFE9a8eX3eEJRCTw5QVpofK69hXdBwKMV4ANKdi"), - pubkey!("mtnDu5GJeWHFXzEwV6RbocsigkGmvDorHj8Tw1SPeYQ"), - pubkey!("9cpGSYpRthttGo3QvidzWbd3nseHP3fGSURQvqsih7dw"), - pubkey!("BdBhzGbdBb2JvaPJbpNxnPKqifWdatvckJoWugqf1gGd"), - pubkey!("7NhPqxVw9VMJhcEsw1NSRkfuChCCrm6vRg4s1uUMrSUY"), - pubkey!("3edXHybYX37pFU6ajwFmouLAaoVT3Y9g5UxwFqhMEqCB"), - pubkey!("DcV4GCNgLv8viyaa9H8Msy6P6U45MbipiHZXjJumoVnb"), - pubkey!("6GQUHWkfKgkFkNdqyStS1Sow1nAvFDCxoY8S6prU3ptQ"), - pubkey!("1andmzF89uqE7HF5uzFLyMPEnoKDjgmaccVu917Esqg"), - pubkey!("9uqkcbU7oecQPdASKr7GrBEteS5BGLEGHeFHWxpNN6sw"), - pubkey!("57KHNNE8MUMxTfee61e3WttDn7xghbHYFqpnN9aVtA7R"), - pubkey!("BoTrd2M287L59VmoHLJXGGj3Zfk8F93QtCPLqasLDPW4"), - pubkey!("2uki6djGnWnC6SfN6MvcZPtn1hZ7N8of54Rimuik2qup"), - pubkey!("4FXiDN7mV3NteqUMrPJsZaUVpn6vsghnYdEnhYJstdyu"), - pubkey!("8RDJcd66btm6UxYozrzkQaVNo21CSsDHWzE7Buy3eyys"), - pubkey!("9XhUhyaaxNLpzy1ZSgAYkyoU4K2kcS7HBp72jSegPy43"), - pubkey!("3ucoQSjg6AVpSotpZRCoHV82v6A1hNyMe6kX8Ag36qG9"), - pubkey!("ECFuMRESmaWivsi7rha4rDmwuKeN8xppEophf6sNLgUx"), - pubkey!("BQbXD9tqv3ysrvojSZao2RoW9ucR4RmiKbKEaVWJp4J3"), - pubkey!("2jVdMx7fb88txbG6YoZzC7kT4Tq8rJDaWrNgbZ3ZnqCb"), - pubkey!("563zd49mogp48wBC4XEjzPQ2hZNViSTPjiN8QUteQjMv"), - pubkey!("Bw5zgt2ewuYDB63VtU5LDNNi8GLUwcEqE9iiuLwWUPyq"), - pubkey!("2naDt6dWy5uF3vbWKVxddTKmdEg3FaF1d89CBe3EaqYn"), - pubkey!("Cy8HEkWe4xqC62A9d4ZUzHS34CugT9SNLxgRazXYibW"), - pubkey!("Gdmd1M2LAYvU3A8R5imZHeNSguVNJDGNhprtyMDADrAq"), - pubkey!("7quEeAYjHqXsrR5MNRtsLKz3vfZL7CtoaLjpaFotqaFu"), - pubkey!("Ecu3AFZM8vHpEEfCDU7ecTMY8eMiXpPk1df3FrTTtR3m"), - pubkey!("Ak9DsM3BQg47CC8YgGbUiMSZgJyUxqjvckeKybeJd8gn"), - pubkey!("E1L58mNnUc9STrauT3TnR8Qb6EGfjqSmHBFNyP81ii1m"), - pubkey!("Enz7GA4a6hZ8UKtM4urov9HnH7PP5kkjs5Yggg7BYeZT"), - pubkey!("BREFixw21JPC1Vwad2tVCCUD4qrD79sKpKmbqgfkrvCz"), - pubkey!("FhEtsokybH9eqEEbreQqxwndUSYz6jvbiYjA75oX5faq"), - pubkey!("G9a3certFcZbWkrsvzq5E62CxgoBtEv5NFT1wjr48SAB"), - pubkey!("6BNk6UhhpaNE81MscSKnTVGnpUeptRyNPDRQxzD42LuH"), - pubkey!("7ry8P6VPABw58QcKjWi2ksgcziqdX5Woc4NubiQWXwBd"), - pubkey!("AjJk2eKmCUUXkcFQASGdcBxNh4UL2MuwQ9Bjdq8RQu5"), - pubkey!("DmzDHcbMFx2buuhWSHq7uk9345A6awrQg5dVLtSWXXa8"), - pubkey!("91wUgZtGXWJzQsFKq1T7xrgTaCYWLsLcwPko38V8FjUR"), - pubkey!("DVB69rna82Y67tn3Sgiy9eTjZmtwPppdsYv5RBLfQfJd"), - pubkey!("Hudzsqx4C99GEhZVzjzsjKZoF6kooUZdBDWTKJFnAcYk"), - pubkey!("F8GDWM1WPmnaRPJRpRuSm2WLfXLQFp2abiBorynpWmvR"), - pubkey!("CQh1EwRmscHSE8jna6aUxAkjvr2VwLK96GWQz3tbmKqA"), - pubkey!("6x6u8VZct4hrhbwUxyATaHNqCZytScFXGWkdwqG7oK3u"), - pubkey!("MadFFXJx5oqYzJyoRtMgDm8PwSc3BwJpvc5uaEiMDuo"), - pubkey!("582Q82NVuUespuZ5xwtJZnR9UuMcmXqmUgSysBSx2VtQ"), - pubkey!("GsPisQ2H2FLifx9T2FkMxXAxBaSCVX4qZNP1yTrS4DUA"), - pubkey!("EcVpECeotzrs2XGcrDeBiDg8FhN55g4aAgjKJja5QQXb"), - pubkey!("2eUFNjWuoSEQTkgvfHdrPcByXkwA7pTBiyZi95u46PJ1"), - pubkey!("4RGNSXy3QJTSeQgmhqKvvNhhkpD7KAb3rok2tWCSQ4zq"), - pubkey!("BobjP8dCMCQ4LeCqymDzHqDcyQNTGBBbo8gNG3N5Tk7t"), - pubkey!("BVKmb7UTRUoQriQMUVfpH8f9jn539Q154jGWzPVRCPQQ"), - pubkey!("VKPqFvbo4DuCTBCs4rvtGDUTdGEAS1R82R6Ep8KnzL8"), - pubkey!("JUskoxS2PTiaBpxfGaAPgf3cUNhdeYFGMKdL6mZKKfR"), - pubkey!("7seVHpqVocNhW93j6GCbX4BLHNoG17ug4s62TCsR4XyC"), - pubkey!("DeKg3DbqfV7bdmHzpkc5GY9R78zKWPLqZyn6sh2TYZmv"), - pubkey!("FLNQ4FHUAy8be2LJgggunjg1tAehVvt1eQDpxjmWVeFA"), - pubkey!("BmnChLJUQBWaHDpwurTtuNpqZkPf6eMNbgxP5Am2pL62"), - pubkey!("2kpF5eiiZjPh3dySyb4eFHg2FFDDyxBAKtf8kox681Lf"), - pubkey!("6fFWA9BLyqNSSRcKvdyGtLrgaTzx31fs2oLaQnrSw3Vr"), - pubkey!("DbswyzwwS1LRYKSxvwJbyE8SVxUYurfWJEu1mUdXytjQ"), - pubkey!("6KW3smHmstPUhAbmq9CohiwbXmWcq81jfRehEE7xQ4ia"), - pubkey!("Bwmo2vpL8QB9PStEbdg4UZhyUwA7q5HHhyJpPVa43Aeq"), - pubkey!("CPYRPM421AaXBM5qGShnWsoiZMVcrCCS3JYfPG7SYYAK"), - pubkey!("3KFstu1jDPb4TByc9r4915kYkghkUNAyzQsASQGuPZWD"), - pubkey!("884aD4A8GyFBxqmuP6QQ1Gi2EwucFykwXFVkdom6uhhu"), - pubkey!("CniJsr5gxFxhL35FVWFtYtutajKjn3wYQt33puTNADTc"), - pubkey!("BixZzjYNyYGi1ywdHHekRCkxcQwNvtCKxxGppNCecYbT"), - pubkey!("387EhfcBR7Tj662V1PrzhbpaHPab4YddAPTKPf21wwkk"), - pubkey!("6jr135RZTwHfTmuxSTbZvoL7TQm1Qd6Koqfu28DaE7XN"), - pubkey!("8PwomUFs26C74SCMF2S4cMx1aujZ3i4WCihE19Ztm7n4"), - pubkey!("3bNurM55JJyHTMYfMJw27gLSHKknthBkhSazAi9WX2or"), - pubkey!("5eefJGcPYCqGWDzUEMpBwRFyE8DfvqwqDk9goFjXGkEn"), - pubkey!("4r2wk9KM1wjrUhmdUpv9SvuFUDwfpRUP8ezxn22j7eXX"), - pubkey!("BVtYZDPpsrVTnwRFNmGDbg64uToGS3mxxY9U5MAZ3Nsw"), - pubkey!("DSZFvyL8vwUZ8i67koaJT6nA26t2VhWftqCrYp2sxbQr"), - pubkey!("RKm9CQxvwi9iN331Rkwktm6achj4JW5pFYsKXb4xfeG"), - pubkey!("2zahht8VNqDoUSgr4gyUXXzkr8xziyD7aTBNi6i9oAHb"), - pubkey!("DubBYMVsQeQdFNMR3ho3fDAQxqSM2SVNc9HsQXnzjJ3F"), - pubkey!("5onZdAogWbxCzebwvfeidNeX9VgkQzbGpDuwS1THkhuV"), - pubkey!("2xTu2WHcvt5VwbdwBr1MkfV72jyy9phd5ksfrGy3tyma"), - pubkey!("7ceTUBq2k8pNjrcjyWHapmhP51J1ttyKUHWk1fcqh176"), - pubkey!("yVqjGGK34PgKwhEaeyp4UTLcQiJQaKTPrnbJADaMj9x"), - pubkey!("2mAShJV8RduZEUMvYWDAZgttRzWp2knc5EHoVszuVoFo"), - pubkey!("6Z6PsSvbQHndjqNTFZUEYLpouFcExYxaceCHAyHGNgMc"), - pubkey!("FTX9nCXBBczYYtFxi1pus76YQt7XupYx4KVY36FoFepT"), - pubkey!("3SK1R1wWABFtCBRpyCUe7vshx2vew4m52rCz5V5Ynt1F"), - pubkey!("BQiMrFT8Kcey131zfk7jKwvS4esvFGmLreNPP6SfoF4R"), - pubkey!("5ytZTkw2noiFm6Zu6pZcQ2Yeg1EBnRnxY4dKpNpYF4qL"), - pubkey!("7qCM9LFQyW49TX7Dp8GaqQ8fytAEEDs2CJ2Td1jLtzXf"), - pubkey!("9ZFLHxu35yZaB1syp7L384PpvtgUX525M38RrA3CM9bh"), - pubkey!("EVrX3x3zfjKjY2baajZGUTwrgbKFkw1iQP7FKAEuRLF4"), - pubkey!("BRa2oSPwuTebwsc28jbtMxsYtuvqbPoJkVM7n9q7zd5o"), - pubkey!("3B4nPAvYGo2iXxvAHRktcFBexAiU2wkqFaywK4wtM1dn"), - pubkey!("6yQQZhoNDd4qLwFn4LcQuH5XEdCRQQgy9o15DMLot4pa"), - pubkey!("CD9QsVyyWvmUnuZeJJhQ2VzWAYvkKiRpCobpJWFv2YkD"), - pubkey!("D1w6h4EqQZw2RYped6Y3RV2jDeF2a4TFkNoySbNi7j9j"), - pubkey!("9KRVJDMDAFHsQ3FD6QXsmQAEf8XAXgFHVWeQcStBrtME"), - pubkey!("73FhRUQPTQrsyVMjLCx64ibqUD1egynyhzrcL3qcW2gi"), - pubkey!("Fj2HgotRPACCTiLK3qXPtrx9ez6EDgVpUKn9NQUa1LRu"), - pubkey!("GpwDBLw12TCKMQkqJEGHppKcV1ULBPfXZV4RFwWftuxY"), - pubkey!("BRgdUnFLFKFNR4WF9eWAesez67D7mU9mri1pjd7sj8P8"), - pubkey!("Gv3mxjsWGH9FFvwdqE5YiihNSGeMaNY4V1A9xxfRtwWB"), - pubkey!("AN3tqhYC8TkugoyFzmMCTwnEaNpDDyTSxBjpvZrpDjuN"), - pubkey!("8fTkqpcqUhAs6KbrbkrZcMLKvrf5WymWizRi7v2TFqBY"), - pubkey!("3nk9Bz8anhjZrRvHd52ZwG3JLasHUFKU1xbo4vB5XekX"), - pubkey!("E31Z3uXrzEFMxscd171QH9aCJZiMWLCGYbVgwcHx2G2z"), - pubkey!("D9FyQGZcmJPEv4gAD7oH8BTpj4VQiwFEKyCvcQMgYAY8"), - pubkey!("CzQYL5eL2ZQH28yvk7brWotjj7TrXaw55nasGnVYyLWn"), - pubkey!("aa1AhZSe87ordRejNkGLaJN8vs71t8WDD3aYfZNT7cf"), - pubkey!("5Z9UpPrFQTeKeJeKrgEWYcSG8MFN3YtAtdWw7JLhLJHL"), - pubkey!("85j4tHFBabkGEnoGPj5eGuYz3oUtqgZcHkft6aYkt1er"), - pubkey!("AeeDSNJicCbwTqDfbF83TA4MFaYP1dZujyTTQm1bdQEe"), - pubkey!("xrykqd9rb64yNZeoxzHLAGFPrFgkN65w3aEJ6KqzN74"), - pubkey!("AkifiiLputLYjN9BdWvQF3ug2odhe2GJBUXmdGVENKsd"), - pubkey!("6jy3U836vS8omfhYt4nEzQyyXdBYzQSiDskSJTUTS71n"), - pubkey!("GRssGjrNG7XtmZZK644VTN8BkA4UEWSmH2cke2Zj6prf"), - pubkey!("2ww2LiHU2hdCCiTQbvQ72PEgUBrznpmLpTUXCBJZzLPa"), - pubkey!("4o8R93WLNpj6kwc8ka8ptLW7dShb2hoi4S3dFLqRyz3E"), - pubkey!("EA7jgT8r5K4r8iFhxhGxRQYYaQK1RoYKyZysaNm5r8ZW"), - pubkey!("Gi2JbovBxEgRyUrf8tg1VE3UZqdQ5fHB4csUKG85cTA4"), - pubkey!("7YMFFjzKFAZVauacGPvtxuVngTbxPsv6EM4oiqjxMYtz"), - pubkey!("CSVmNyCwaMtJbvYCH7JbN7GXF5TABrdTUgxy4VpHQ8Kb"), - pubkey!("2jWN7kds7jBYmHr4fCXu1gMJUkgqJCJoVwJ1Rv996jSS"), - pubkey!("663A19n8DaCXdaSycFwmTxXeNBKFMZunoPRoy6LDbK6k"), - pubkey!("E3onGu8Fe2bd3pCpFaXmApaUB3JuB9neCqx6YTh3Lo6R"), - pubkey!("E4DwabawCYTZwqNv5LVj3LHZzPP8XL6xuya2C6mkJMwZ"), - pubkey!("5YLUmcpRKyY1YQESMTFiwoA8moTGcjSLadoRC9PxgC5P"), - pubkey!("2RiqNWDWgJnrX2G3ahBJYmt3ab56K5HXFzT7WGWf7h4X"), - pubkey!("HAXeS5472XnL58J5Wq83qc1rZqc2Dud7Yo44A6aMakDs"), - pubkey!("8LNCzYSVijuFDxHH1K62B5jo72W7gxicB3QURBfHkMbj"), - pubkey!("5MRLosQFPcmqL2DNBLKmW5dmmyXF9jjx7sA8QK8Btd7Z"), - pubkey!("13V7kcaGMsXHTbZNFStFySXA2yoLAupHXBkPG61AV3F2"), - pubkey!("EcSUyx1axTPa7CtbiNiKo8c9YTmSigLyPG9mLBRmfts2"), - pubkey!("5qrvvuCB5Lo75tdyD2kRaGASDanZh9b6EX4VSWWmuXEf"), - pubkey!("58uP5UWXaZVv19uoGX4MqqRS2L6bA7tGmxxZK51QXFKL"), - pubkey!("6EFuKUZEaUCGHsmei3NEJhgDX3458YTkEfQboGNj5x6n"), - pubkey!("75NTaCnZU42fsjqEdr8gKbmx9JHUXeeXzXFA7HG8JNea"), - pubkey!("5TkzZbno1x5t5MBEoBDXdicckAoMCABnWVLS2ntbk6hF"), - pubkey!("CK8ei4KVemer7GznCQK9eu3BRQMBLXXieMAbJAeHMcoN"), - pubkey!("E9sTbkJ4Sxcvmy774auV7wxh6vKt9TXV666z4WR4NybT"), - pubkey!("E79uaVcz8KpQUSNVA2JGX5rqM6DuWm3KZThNvBoqGRFn"), - pubkey!("93skKa6mstJdK9LF1UDRVEv9QVpD8738JpfTtHmxBTzx"), - pubkey!("CiBaXEQxvStqkSEGAYPX6YdCnU4JzndLgFNGh1Fr4Pd"), - pubkey!("Eu3KDfvMU4PXSqNHFU4MFVLfR3BDwm3cdNqMgEwvqpiN"), - pubkey!("5fAtP5JMbMjVuz3Dt4XbnjdvQQaXKopuuJqEkDYumy6o"), - pubkey!("6Eqy9tnGg2RMqwM8i1NXCDj37EfAzniKNd87jWA6Nhou"), - pubkey!("DgXUUwsEw88fbDi7FmASgJfDR1mFYJnUBSpT46aBtp35"), - pubkey!("GkPpc1auG5FPgqQXYytEUUndZstZESh3h3bPPJ9jyZ4b"), - pubkey!("8fXvsNFwihfyKRDEv4oAhM2E5NWSNBMByKitZHpUD7Tj"), - pubkey!("CX47idzWhK8cDLpUNYraPFoieGxy8wzR9XJF3Avsq6nF"), - pubkey!("HoeivdxHbL7pFY7CyhZP9G8eBHouRw3ckkSnekj7Zh6u"), - pubkey!("69m9TPRfb2oYdquNDKu7XRfSaYyKqv9D2MamcyWSeGj7"), - pubkey!("8525tNnMAxPLqNX3t8EqgRzhKvUQo6oUV8ovUnLWPWD9"), - pubkey!("FzfTigLWBhVzmStXoU91tBoWXjKqqyEMLzPMuwm8xj53"), - pubkey!("5rUNTDMAFDdvSQEHSpNNtUVRo5aKdPfLRsKsWm9t4Lv7"), - pubkey!("FKSNp1wnJMabfTPJBD4M2WVPTMwJoeJ7gDiGu1ZW6nFe"), - pubkey!("8TaGL4S4vx3tyZcnr9fV6A999MfCsdC14QLAB2F9eVnm"), - pubkey!("UuGEwN9aeh676ufphbavfssWVxH7BJCqacq1RYhco8e"), - pubkey!("HW252Pdt9qsyakaBepVWtcEKesMLJKCnYJy9MmZNtng1"), - pubkey!("5YLjGCCdStdL1WfAmeSUU6P2X71Rkc1poxKidcbA3KK5"), - pubkey!("AwScekxPFJchPcWkwuKMb8aK8fjEw5o7BGTM7eoofk9A"), - pubkey!("Hzs2oCTDfEXQc28wMzMujKpDYMMotYaRkdQdtHZNjh3H"), - pubkey!("H7QppzedhVRWbG4vGRVGSPP8pcmBFqDKC31xTEjLnQHA"), - pubkey!("GjmLjF2vhmLgbEy5VgEZz7gWEzyCqZAXMBsKBsSk5ev4"), - pubkey!("HzJr3T2qHesCVkQZcQJK8nZWkns1N9qj4Hj4dHi8ZNP8"), - pubkey!("BQQYZKHfNhmUrdU2UhwZzWdVpiVzdBCa3qLerhDrcbAs"), - pubkey!("7LfVG4MNxTenrhw4xczt3ToFL6jd3KUXSzKBMpxambtM"), - pubkey!("DcBJoxBb8KNqrwdbMKFSR8qjyYYpTy1Pu7vU5K7LZWNn"), - pubkey!("DGs8ZrfatMpLza4ekXDdq5mmEmt5QqeKQ1q3WrpsrM3s"), - pubkey!("4DGxxu1fTbteKXm6USy3enW7S18iPFuJkqhKrSopGeBS"), - pubkey!("AkmZXNFjEL6LgxGi1eM81iTXfwNJgeFG7iXUiFfmHni8"), - pubkey!("297ogus15jvgePqXZCwT8nB1gvwgCJYdKcuXKiyH4TfS"), - pubkey!("3yKHWBKD5DeX7vG2ESJwWQWF4HgmgHumeXPhaZnqiore"), - pubkey!("4AQBsVmECmSBPh4JicNhGaT9waHETNxeNkaz72tezgSR"), - pubkey!("6zWbGFC9WgPymyrTFM2MKAwLu8vKXwZJjFUcksikdabE"), - pubkey!("Xp7Swytm55aTD8onDegFAVm4gC7zdCCkYMobRg5oHfr"), - pubkey!("3rB1eaJeFYKHwRP1q9mAVEYxY2cCJkHx98yVppB6tPuu"), - pubkey!("ABcnheNNaj3q3Yi1pbDrEhSjKJYqJcn4RY4tZcurdXYz"), - pubkey!("CwkzrFZmFPqX1x52uqRV3d1JncgmMiCKTxScJ3XSVA4A"), - pubkey!("3t9CReK1B7z1B4sTnfWrDZtyGaX5pV6ydXvE1vanJ4FC"), - pubkey!("G8d3gRGvAg8k9oiazKPPAA49BgAJuPACFbdf5CRWjpaZ"), - pubkey!("gpoo1atPkrKnfxQ4Qt214ErbgBBJeiksL1EjqBHynbo"), - pubkey!("CFRt6hxJoYQYgS9jVZHQEYMpEzfRLxAo1vwq7R8PnLJ7"), - pubkey!("D4b7ocAwoUrCM7pZZYfqyAftHXDVhNwbw9JmnrAHG6z6"), - pubkey!("BntbD7PCAXPhXANT2XibNgduJS1UL2dysUEPMd9gLeJy"), - pubkey!("3rcwuJQTBG9D5d6P2TurtQQTaTVkw4HPufWGCdhcGqfm"), - pubkey!("BLtavXya3V2o9BvREamtdn2tgG3o2tGp2qpKr2VbavWg"), - pubkey!("91rV3hD3ZfMC1smebh6nix4mSUePXLewsxj1Ncz79LD2"), - pubkey!("6MRghTVnMEXiubLs6V8s3bzz2FEke5UugwR7ByuioB9H"), - pubkey!("7HZ222sahftEBj8JdyHgvy1oGQTMYjtdNSUZDqzfNyiD"), - pubkey!("G5SXciAG8aacJ5F4nRAPzDXfn3Rjfbwn1EMvEccrFnjZ"), - pubkey!("EoB4LgmryBX1m5YmZe8wohQzFssjMhEK1DWwcnk5Gmo9"), - pubkey!("GtCneCahPq4m88zQxKVKnZM72qdbSUEDp82UDu8iNLWE"), - pubkey!("HfuUoJHkdxnXb3wCG4roMkeBA8nX4XpqFm9VDwtNQtT9"), - pubkey!("GbD6i4SxcD2Sqae3wCPvvHpNtr9LxahtHHC8wfThmr95"), - pubkey!("G4WwpzfCXPzAJ2jfLXZRk7gNTEfaD8jNss5Xij33N21e"), - pubkey!("Bo4nGugF3usS4N797H4LEm2r2d794kvht6HruwNCVe7Z"), - pubkey!("2V1pTma3ZcctFvT1tALnoc2W2u5W1DmDqi3BjqerHrCN"), - pubkey!("BX2X2QYU3twF4bRX2Pro4ARUNXi9cDd7cpRdZFW9JWC8"), - pubkey!("DF4Ad2CRWyR5KMgGaqcfG258twr9LmsVc7hSCk8Pizfb"), - pubkey!("AonibGzhwQ2MTtYNXiaEPnrfQ2c1eqJwp8SDfZL46TL3"), - pubkey!("5o6PCwxNYpoa6wdtvaTiYyKrE9FoNE1bT542ZTuNgJpr"), - pubkey!("AcVvuR5PoA1Mq5W9UY9x6fEAJthK9a1R4QHWQmETDb8h"), - pubkey!("367Hqa8Q1DY1p5jKEmpCcJiyj5fH1dfbD7ZUBhCBbEa7"), - pubkey!("GmRC6EhKtBEcKM1bjCBzamWDy3qmP5z2Kc9tYjQuc2Pn"), - pubkey!("4iZPMVzyGGTnF3hA45vrSFP32tZ7yKiyQd7MiJHv8dyF"), - pubkey!("3yxPRUpxUL2hLMLzDTWQpAFqY82MCphgy6iik3J8ZDLr"), - pubkey!("31KPvwdWKK9FVPAER5fXLHfCCBQ5wGyUSqFMnpF9Xvyj"), - pubkey!("4KVqRjx2Dyo53wANTGh5QKQQbatqUj3wfP9ZV7YinJTP"), - pubkey!("EEPoEVgsabibKaAxs21JPBCMqKHmDcWkWzvXjS8vPf6L"), - pubkey!("9xUqwnwaHnmXJifpJbu2dYu2PybVE2RDZhB7SHhLU2tL"), - pubkey!("27R5t6DAWFnMXP3ZHA4aXabtxZ2nP4qH3BVQN1oEWSkj"), - pubkey!("4hewNZbUaUziPGQ1yTmSX2yx9syzkc664RNjfFFS9sK4"), - pubkey!("EpkrcNkBaK5eLwMKpV52H5MjPxcdWxnb1FcBr9FUEmMt"), - pubkey!("6pKuMtqh56yaWT6ToYk3F7WcZnowAv2DPuptkYq9pPKc"), - pubkey!("2icoVmEXH2q4zMmHChnFPS6iDt3hNTPrz3pdEEeK3Dqo"), - pubkey!("gjwc2FafGF46Yn8aSnQu3a9S5MN1knwkc99yTzyqHnR"), - pubkey!("A4ke6mJAL7muUJf4QfUV8yLMr2A3nH5Qnu1shV1DefvV"), - pubkey!("CpF8aa81tZ6uAEoHHK5N79SREL49JyZxjMLPssY6qqU1"), - pubkey!("8ujTq7pjihTBbebirvHohqykFLvNLa37MLTtFQcp9QHa"), - pubkey!("3rXinbzxFTQ8uJTEDbU4XUD4gYBaRkfg6DDqpEZKwepf"), - pubkey!("9zKXwnQU4F2yr2NdAAfzu6XGJQCWmgM5fMyn9qsRVyhj"), - pubkey!("AVYG9UHetNHT1FEDPLv9pN2sCCH4CLsjvkzGjVXBfEiS"), - pubkey!("82dYc4N5KJvMyAiSRC74D3uHyNGy8Wr4ghfer15YLQa6"), - pubkey!("GEwUNpFEN4q4i8RBxmjQHtLC57tmQkUVDhFrDiLjxv5P"), - pubkey!("B8esH7ZNMHVwm3gQuzeM3XEa8pUa1ELRCYTezwRsMcjA"), - pubkey!("6Mn9Th41tmNqXyPS8y6hno2EB6wzHcYkfnh6br3NGNAy"), - pubkey!("ECHb13JeXfPdj5Z3EEFQ1vcGEpagqHXrWGZF7NdaWF4z"), - pubkey!("FBsBn9iScLsvSe9oUtQiXuyXGh2uJZfwAAzzD8gP84AU"), - pubkey!("Ark3fRdZ2uPGmnVtpbi26eFW7WCHQh3jujvGuQbTVMHx"), - pubkey!("CDL3egQYuQDEk3XgAELmhXw4YTvLGNi1tk1UZBSNnYuw"), - pubkey!("HdYJ76t95FhMaagrZaX7GhS9xXduuaQeVzrxqaRsQpfY"), - pubkey!("2pUaXgALLnnxYAPBaLT9R72NCZJshrXkTTUmQTnJUsxF"), - pubkey!("4LVEx7bZ9PEVP1o9xsbN9XbbHuZH3T4t94AkWJAkMRd4"), - pubkey!("6Fg49wV8MGW5PeTTF1LoFRR8FzUGiMZXC3gWZiagAWzg"), - pubkey!("BwCwfRRe2y6Rxv6SQZMRQ499TTakKnKEDbPofN8JQ7p"), - pubkey!("5D1oAw6sE14YvdSPwGWGbFCj7SVTbivnTxWXxbvNrur6"), - pubkey!("CqJ5a1XB3c2SLcAcxAvZQ4hzYmLTSAtMA6RBy8ovWmeY"), - pubkey!("273HADhxAy3NWKdRiTTZQG6WukHz9TiJZeiQjnSe2pxG"), - pubkey!("2JVo9kDtAn77vQC9KLrPBmJ2hnCnjaPpgQym6sCKnGRj"), - pubkey!("DW9ugDkNyPxNz5egzBVV4EBdWMKv5hUbLgadA7TZysyg"), - pubkey!("9W3B4bKyqBrGjaJKEjLStuRmLTf42fbSqCEiML7LHaoe"), - pubkey!("9g4VpomWV3HoX6AgsjAVumysrLzMn49SxSbHm5xP8SUJ"), - pubkey!("6ptnvnBHhR7bjgLK5ZimxKFhhHzNWeEdeNB8oZiCeaLM"), - pubkey!("AGruT4qWZf3FNQEQewLw5NfFpMrMjDhgB8H8UGKQPwms"), - pubkey!("7p2vJUJPG9pfRrzVans3zmYe1oDZdzS5YjYM8dP3L4EZ"), - pubkey!("5SwFSBuYTmGcHvLbL9tQjZRKNgGNijzrCMVoQrrjXUSg"), - pubkey!("CZdNF9Hz9ayGdDhVDvK4KhKCX2PS7BNkEp7cwJggAkvi"), - pubkey!("8F18uJ2JaTGq38pL7qsdyP6TJkyxeSXQd1LcHmg8FpEZ"), - pubkey!("9vMYgsTUpGSHnqjrgkQekzFq23eRpENnUESyZ3B9BzVT"), - pubkey!("CXcLK7roVYsEZnN66fWq7h2DKSkfLmWaM3P9CvqfeQRh"), - pubkey!("HPK2t9HkM4g2cVn4yaHz7tWwYLp9KwJFM4DTZkyBrwg5"), - pubkey!("RX71VvEgsKh4fePR1XjywC3Nr9hmjSiBgcskCDMoGoH"), - pubkey!("E8x8J8nJMNWrQXQF1BdBSY9QA12S9qmUycJPPjQhvz9H"), - pubkey!("5EFvY7HwqUh5vfkcDtQAbQoNCdiEWL6eqG1zuiRihzk5"), - pubkey!("BgVtFam2Xvj5vte1a5efvMC47uVdNugx6bqGzDLkBo6i"), - pubkey!("AANrdR8Ks46B3sGdXdFTF1ZUgKAv5TZYwvMYaChTBVnE"), - pubkey!("Fe1yc2UThPB8fVhE4TnyCLvFrD2TFUuKkaQEBRGeoVJ1"), - pubkey!("6c4PrZ7xbRaaS5ezpCEZ78wCdCFA3SHMy9ALNDjGnYnk"), - pubkey!("GddD396SuoQG6pNz9PzFZ2AVAFJku99FRJ3pLpePd2dU"), - pubkey!("9CQAdxRFesp3h16eup1EA7y9hi6JL1KK6HGPKTNoF3yH"), - pubkey!("NiMA3shJChU4fE1F2TVy71uHUwdg41e56rzw3yHMNVz"), - pubkey!("EoB4LgmryBX1m5YmZe8wohQzFssjMhEK1DWwcnk5Gmo9"), - pubkey!("HJdo74b4sjmCKTaBEVHPjpVAty6NEc9tMctf7QTS4Rmr"), - pubkey!("8Kisz41r9Fzxbg7L89R244Rc7PRYSsAZEjdyAfvf5sap"), - pubkey!("4kTfKEFoEdWAqbgfwEtLuUoRCemLNHDGs8DBUdkF1kqs"), - pubkey!("29dJrNjqxT2x69PU1nepPWpkXdiFsrUjeAufhkiACpb8"), - pubkey!("RdqkUDn71DBMpS1vqHx3BwpaKTWVnRNznmxTuWJraEW"), - pubkey!("8u9go8TFX8uwUhUCEbL5nYwkQENaaVvjFGuzLCHPSD8j"), - pubkey!("GLfSX4YtaFXQrH9S87i1HuwxhX5UtznUvsWyEXvzWzpY"), - pubkey!("L9q5HuevjsMoe6jLUVWnADMvyRigk3L9iSzbhKcYK9v"), -]; +pub const AUTHORIZED_ACCOUNTS: [Pubkey; 1] = + [pubkey!("pqspJ298ryBjazPAr95J9sULCVpZe3HbZTWkbC1zrkS")]; diff --git a/program/src/withdraw.rs b/program/src/withdraw.rs new file mode 100644 index 0000000..2accd48 --- /dev/null +++ b/program/src/withdraw.rs @@ -0,0 +1,78 @@ +use ore_api::prelude::*; +use solana_program::log::sol_log; +use spl_token::amount_to_ui_amount; +use steel::*; + +use crate::AUTHORIZED_ACCOUNTS; + +/// Withdraws ORE from the staking contract. +pub fn process_withdraw(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { + // Parse data. + let args = Withdraw::try_from_bytes(data)?; + let amount = u64::from_le_bytes(args.amount); + + // Load accounts. + let clock = Clock::get()?; + let [signer_info, mint_info, recipient_info, stake_info, treasury_info, treasury_tokens_info, system_program, token_program, associated_token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + signer_info.is_signer()?; + mint_info.has_address(&MINT_ADDRESS)?.as_mint()?; + recipient_info + .is_writable()? + .as_associated_token_account(&signer_info.key, &mint_info.key)?; + let stake = stake_info + .as_account_mut::(&ore_api::ID)? + .assert_mut(|s| s.authority == *signer_info.key)?; + let treasury = treasury_info.as_account_mut::(&ore_api::ID)?; + treasury_tokens_info + .is_writable()? + .as_associated_token_account(&treasury_info.key, &mint_info.key)?; + system_program.is_program(&system_program::ID)?; + token_program.is_program(&spl_token::ID)?; + associated_token_program.is_program(&spl_associated_token_account::ID)?; + + // Check whitelist + if !AUTHORIZED_ACCOUNTS.contains(&signer_info.key) { + return Err(trace("Not authorized", OreError::NotAuthorized.into())); + } + + // Open recipient token account. + if recipient_info.data_is_empty() { + create_associated_token_account( + signer_info, + signer_info, + recipient_info, + mint_info, + system_program, + token_program, + associated_token_program, + )?; + } + + // Deposit into stake account. + let amount = stake.withdraw(amount, &clock, treasury); + + // Transfer ORE to recipient. + transfer_signed( + treasury_info, + treasury_tokens_info, + recipient_info, + token_program, + amount, + &[TREASURY], + )?; + + // Log withdraw. + sol_log( + &format!( + "Withdrawing {} ORE", + amount_to_ui_amount(amount, TOKEN_DECIMALS) + ) + .as_str(), + ); + + Ok(()) +}