From 47e6672aac3571f412e2beae6d643e96f9e320a9 Mon Sep 17 00:00:00 2001 From: Hardhat Chad Date: Sat, 24 May 2025 16:37:30 -0700 Subject: [PATCH] cli --- Cargo.lock | 5 + Cargo.toml | 2 + api/src/sdk.rs | 4 +- cli/Cargo.toml | 15 +- cli/src/main.rs | 357 +++++++++++++++++++++++++++++++++++++++++-- localnet copy.sh | 25 --- localnet.sh | 20 +++ program/src/bet.rs | 14 +- program/src/bury.rs | 2 +- program/src/reset.rs | 4 +- 10 files changed, 394 insertions(+), 54 deletions(-) delete mode 100755 localnet copy.sh diff --git a/Cargo.lock b/Cargo.lock index 9ffb170..6216e4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2510,15 +2510,20 @@ dependencies = [ name = "ore-cli" version = "3.7.0" dependencies = [ + "anyhow", "base64 0.22.1", "bincode", "bytemuck", "ore-api 3.7.0", + "ore-boost-api", "serde_json", + "solana-account-decoder", "solana-client", "solana-program", "solana-sdk", "spl-associated-token-account", + "spl-token 4.0.2", + "steel", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index f7303c4..91d1532 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ readme = "./README.md" keywords = ["solana", "crypto", "mining"] [workspace.dependencies] +anyhow = "1.0" array-const-fn-init = "0.1.1" bincode = "1.3.3" bytemuck = "1.14.3" @@ -25,6 +26,7 @@ mpl-token-metadata = "5.1" num_enum = "0.7.2" ore-api = { path = "api" } ore-boost-api = "4.0.0-alpha" +solana-account-decoder = "^2.1" solana-program = "^2.1" solana-client = "^2.1" solana-sdk = "^2.1" diff --git a/api/src/sdk.rs b/api/src/sdk.rs index 1c8630b..321b21f 100644 --- a/api/src/sdk.rs +++ b/api/src/sdk.rs @@ -11,7 +11,7 @@ use crate::{ pub fn bet(signer: Pubkey, mint: Pubkey, amount: u64, round: u64, seed: [u8; 32]) -> Instruction { let sender = spl_associated_token_account::get_associated_token_address(&signer, &mint); let block = block_pda().0; - let block_bets = spl_associated_token_account::get_associated_token_address(&signer, &mint); + let block_bets = spl_associated_token_account::get_associated_token_address(&block, &mint); let wager = wager_pda(round, seed).0; Instruction { program_id: crate::ID, @@ -21,7 +21,7 @@ pub fn bet(signer: Pubkey, mint: Pubkey, amount: u64, round: u64, seed: [u8; 32] AccountMeta::new(block_bets, false), AccountMeta::new(sender, false), AccountMeta::new(wager, false), - AccountMeta::new_readonly(spl_associated_token_account::ID, false), + AccountMeta::new_readonly(system_program::ID, false), AccountMeta::new_readonly(spl_token::ID, false), AccountMeta::new_readonly(sysvar::slot_hashes::ID, false), ], diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 153f152..c478fb6 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -14,13 +14,18 @@ no-entrypoint = [] default = [] [dependencies] +anyhow.workspace = true base64 = "0.22.1" bincode = "1.3.3" -bytemuck = { workspace = true } +bytemuck.workspace = true ore-api = { path = "../api" } +ore-boost-api.workspace = true serde_json = "1.0.140" -solana-client = { workspace = true } -solana-sdk = { workspace = true } -solana-program = { workspace = true } +solana-account-decoder.workspace = true +solana-client.workspace = true +solana-sdk.workspace = true +solana-program.workspace = true +spl-token.workspace = true spl-associated-token-account.workspace = true -tokio = { workspace = true } +steel.workspace = true +tokio.workspace = true diff --git a/cli/src/main.rs b/cli/src/main.rs index 2b9c1ee..5b61b29 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,9 +1,20 @@ -use solana_client::nonblocking::rpc_client::RpcClient; +use ore_api::{prelude::*, sdk::*}; +use solana_account_decoder::UiAccountEncoding; +use solana_client::{ + client_error::{reqwest::StatusCode, ClientErrorKind}, + nonblocking::rpc_client::RpcClient, + rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}, + rpc_filter::{Memcmp, RpcFilterType}, +}; use solana_sdk::{ compute_budget::ComputeBudgetInstruction, + keccak::hashv, + native_token::{lamports_to_sol, sol_to_lamports}, + pubkey::Pubkey, signature::{read_keypair_file, Signer}, transaction::Transaction, }; +use steel::{AccountDeserialize, Clock, Discriminator, Instruction}; #[tokio::main] async fn main() { @@ -13,27 +24,349 @@ async fn main() { // Build transaction let rpc = RpcClient::new(std::env::var("RPC").expect("Missing RPC env var")); - let cu_budget_ix = ComputeBudgetInstruction::set_compute_unit_limit(1_400_000); - let ix = match std::env::var("COMMAND") + match std::env::var("COMMAND") .expect("Missing COMMAND env var") .as_str() { - "initialize" => ore_api::sdk::initialize(payer.pubkey()), - // "payout" => ore_api::sdk::payout(payer.pubkey()), - // "reset" => ore_api::sdk::reset(payer.pubkey()), + "initialize" => { + let ix = initialize(payer.pubkey()); + submit_transaction(&rpc, &payer, &[ix]).await.unwrap(); + } + "payout" => { + let ix = payout(payer.pubkey(), Pubkey::new_unique(), Pubkey::new_unique()); + submit_transaction(&rpc, &payer, &[ix]).await.unwrap(); + } + "reset" => { + let ix = reset(payer.pubkey(), ore_boost_api::state::config_pda().0); + submit_transaction(&rpc, &payer, &[ix]).await.unwrap(); + } + "block" => { + let block = get_block(&rpc).await.unwrap(); + println!("Block: {:?}", block); + } + "crank" => { + crank(&rpc, &payer).await.unwrap(); + } + "bet" => { + bet(&rpc, &payer, sol_to_lamports(1.0)).await.unwrap(); + } + "close" => { + close_all_wagers(&rpc, &payer).await.unwrap(); + } + "wagers" => { + let wagers = get_block_wagers(&rpc).await.unwrap(); + println!("Wagers: {:?}", wagers); + } _ => panic!("Invalid command"), }; - let blockhash = rpc.get_latest_blockhash().await.unwrap(); +} + +async fn crank( + rpc: &RpcClient, + payer: &solana_sdk::signer::keypair::Keypair, +) -> Result<(), solana_client::client_error::ClientError> { + loop { + let block = get_block(rpc).await?; + let clock = get_clock(rpc).await?; + + if clock.slot >= block.ends_at { + // Time to payout and reset + let payout_ix = build_payout_ix(rpc, payer).await?; + let reset_ix = reset(payer.pubkey(), ore_boost_api::state::config_pda().0); + submit_transaction(rpc, &payer, &[payout_ix, reset_ix]).await?; + println!("Submitted payout and reset transaction"); + } else { + // Calculate and print time remaining + let slots_remaining = block.ends_at.saturating_sub(clock.slot); + let seconds_remaining = (slots_remaining as f64) * 0.4; + println!( + "Time until payout: {:.1} seconds ({} slots) – {} wagers – {} SOL", + seconds_remaining, + slots_remaining, + block.bet_count, + lamports_to_sol(block.total_bets) + ); + } + + // Wait 3 seconds before next check + tokio::time::sleep(std::time::Duration::from_secs(3)).await; + } +} + +async fn bet( + rpc: &RpcClient, + payer: &solana_sdk::signer::keypair::Keypair, + amount: u64, +) -> Result<(), solana_client::client_error::ClientError> { + // Get current block to get round number + let block = get_block(rpc).await?; + + // Create WSOL ATA if it doesn't exist + let wsol_ata = spl_associated_token_account::get_associated_token_address( + &payer.pubkey(), + &spl_token::native_mint::ID, + ); + let create_ata_ix = spl_associated_token_account::instruction::create_associated_token_account( + &payer.pubkey(), + &payer.pubkey(), + &spl_token::native_mint::ID, + &spl_token::ID, + ); + let mut ixs = match rpc.get_account(&wsol_ata).await { + Ok(_) => vec![], + Err(_) => vec![create_ata_ix], + }; + + // Wrap SOL + let wrap_ix = solana_sdk::system_instruction::transfer( + &payer.pubkey(), + &spl_associated_token_account::get_associated_token_address( + &payer.pubkey(), + &spl_token::native_mint::ID, + ), + amount, + ); + let sync_native_ix = spl_token::instruction::sync_native( + &spl_token::ID, + &spl_associated_token_account::get_associated_token_address( + &payer.pubkey(), + &spl_token::native_mint::ID, + ), + ) + .unwrap(); + + // Build bet instruction + let seed = generate_seed(&payer, &block); + println!("Seed: {:?}", seed); + let ix = ore_api::sdk::bet( + payer.pubkey(), + spl_token::native_mint::ID, + amount, + block.current_round, + seed, + ); + ixs.push(wrap_ix); + ixs.push(sync_native_ix); + ixs.push(ix); + + // Submit transaction + submit_transaction(rpc, payer, &ixs).await?; + println!("Placed bet of {} lamports", amount); + + Ok(()) +} + +async fn build_payout_ix( + rpc: &RpcClient, + payer: &solana_sdk::signer::keypair::Keypair, +) -> Result { + let block = get_block(rpc).await?; + let wagers = get_block_wagers(rpc).await?; + + // Return early if no wagers + if block.total_bets == 0 { + return Ok(payout( + payer.pubkey(), + Pubkey::new_unique(), + Pubkey::new_unique(), + )); + } + + // Get blockhash + let solana_block = rpc.get_block(block.ends_at).await?; + let blockhash = solana_block.blockhash; + let noise = hashv(&[&block.noise, blockhash.as_ref()]).to_bytes(); + + // Calculate the random number. + let x = u64::from_le_bytes(noise[0..8].try_into().unwrap()); + let y = u64::from_le_bytes(noise[8..16].try_into().unwrap()); + let z = u64::from_le_bytes(noise[16..24].try_into().unwrap()); + let w = u64::from_le_bytes(noise[24..32].try_into().unwrap()); + let roll = (x ^ y ^ z ^ w) % block.total_bets; + + // Find the winning wager + let mut winner = None; + for (pubkey, wager) in wagers { + if roll >= wager.cumulative_bets && roll < wager.cumulative_bets + wager.amount { + println!("Roll: {}, Winner: {:?}", roll, pubkey); + winner = Some((pubkey, wager)); + break; + } + } + + // Build payout instruction + let ix = if let Some((pubkey, wager)) = winner { + payout( + payer.pubkey(), + pubkey, + spl_associated_token_account::get_associated_token_address( + &wager.authority, + &spl_token::native_mint::ID, + ), + ) + } else { + payout(payer.pubkey(), Pubkey::new_unique(), Pubkey::new_unique()) + }; + + Ok(ix) +} + +async fn close_all_wagers( + rpc: &RpcClient, + payer: &solana_sdk::signer::keypair::Keypair, +) -> Result<(), solana_client::client_error::ClientError> { + let block = get_block(rpc).await?; + let wagers = get_my_wagers(rpc, payer).await?; + let mut ixs = vec![]; + for (pubkey, wager) in wagers { + if wager.round != block.current_round { + let ix = ore_api::sdk::close(payer.pubkey(), pubkey); + ixs.push(ix); + } + } + submit_transaction(rpc, payer, &ixs).await?; + Ok(()) +} + +async fn get_block(rpc: &RpcClient) -> Result { + let block_pda = ore_api::state::block_pda(); + let account = rpc.get_account(&block_pda.0).await?; + let block = Block::try_from_bytes(&account.data).unwrap(); + Ok(*block) +} + +async fn get_clock(rpc: &RpcClient) -> Result { + let data = rpc + .get_account_data(&solana_sdk::sysvar::clock::ID) + .await + .unwrap(); + let clock = bincode::deserialize::(&data).unwrap(); + Ok(clock) +} + +async fn get_block_wagers( + rpc: &RpcClient, +) -> Result, solana_client::client_error::ClientError> { + let block = get_block(rpc).await?; + let filter = RpcFilterType::Memcmp(Memcmp::new_base58_encoded( + 56, + &block.current_round.to_le_bytes(), + )); + let wagers = get_program_accounts::(rpc, ore_api::ID, vec![filter]) + .await + .unwrap(); + Ok(wagers) +} + +async fn get_my_wagers( + rpc: &RpcClient, + payer: &solana_sdk::signer::keypair::Keypair, +) -> Result, solana_client::client_error::ClientError> { + let filter = RpcFilterType::Memcmp(Memcmp::new_base58_encoded( + 16, + &payer.pubkey().to_bytes().as_ref(), + )); + let wagers = get_program_accounts::(rpc, ore_api::ID, vec![filter]) + .await + .unwrap(); + Ok(wagers) +} + +fn generate_seed(payer: &solana_sdk::signer::keypair::Keypair, block: &Block) -> [u8; 32] { + solana_sdk::hash::hash( + &[ + payer.pubkey().to_bytes().as_ref(), + block.current_round.to_le_bytes().as_ref(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + .to_le_bytes() + .as_ref(), + ] + .concat(), + ) + .to_bytes() +} + +async fn submit_transaction( + rpc: &RpcClient, + payer: &solana_sdk::signer::keypair::Keypair, + instructions: &[solana_sdk::instruction::Instruction], +) -> Result { + let blockhash = rpc.get_latest_blockhash().await?; + let mut all_instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit(1_400_000)]; + all_instructions.extend_from_slice(instructions); let transaction = Transaction::new_signed_with_payer( - &[cu_budget_ix, ix], + &all_instructions, Some(&payer.pubkey()), - &[&payer], + &[payer], blockhash, ); - // Send transaction match rpc.send_and_confirm_transaction(&transaction).await { - Ok(signature) => println!("Transaction succeeded! Signature: {}", signature), - Err(err) => println!("Transaction failed: {}", err), + Ok(signature) => { + println!("Transaction submitted: {:?}", signature); + Ok(signature) + } + Err(e) => { + println!("Error submitting transaction: {:?}", e); + Err(e) + } + } +} + +pub async fn get_program_accounts( + client: &RpcClient, + program_id: Pubkey, + filters: Vec, +) -> Result, anyhow::Error> +where + T: AccountDeserialize + Discriminator + Clone, +{ + let mut all_filters = vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded( + 0, + &T::discriminator().to_le_bytes(), + ))]; + all_filters.extend(filters); + let result = client + .get_program_accounts_with_config( + &program_id, + RpcProgramAccountsConfig { + filters: Some(all_filters), + account_config: RpcAccountInfoConfig { + encoding: Some(UiAccountEncoding::Base64), + ..Default::default() + }, + ..Default::default() + }, + ) + .await; + + match result { + Ok(accounts) => { + let accounts = accounts + .into_iter() + .map(|(pubkey, account)| { + let account = T::try_from_bytes(&account.data).unwrap().clone(); + (pubkey, account) + }) + .collect(); + Ok(accounts) + } + Err(err) => match err.kind { + ClientErrorKind::Reqwest(err) => { + if let Some(status_code) = err.status() { + if status_code == StatusCode::GONE { + panic!( + "\n{} Your RPC provider does not support the getProgramAccounts endpoint, needed to execute this command. Please use a different RPC provider.\n", + "ERROR" + ); + } + } + return Err(anyhow::anyhow!("Failed to get program accounts: {}", err)); + } + _ => return Err(anyhow::anyhow!("Failed to get program accounts: {}", err)), + }, } } diff --git a/localnet copy.sh b/localnet copy.sh deleted file mode 100755 index a191533..0000000 --- a/localnet copy.sh +++ /dev/null @@ -1,25 +0,0 @@ -solana-test-validator \ - -r \ - --bpf-program oreV2ZymfyeXgNgBdqMkumTqqAprVqgBWQfoYkrtKWQ target/deploy/ore.so \ # ORE program - --url https://api.mainnet-beta.solana.com \ # Set copy of mainnet - --clone-upgradeable-program BoostzzkNfCA9D1qNuN5xZxB5ErbK4zQuBeTHGDpXT1 \ # Boost program - --clone-upgradeable-program Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB \ # Meteora pools program - --clone-upgradeable-program 24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi \ # Meteora vault program - --clone 2G3WKgP9Eemzgbzk5B4w2JSAizjWfEmEwa4JMfzLyXzM \ # ORE config - --clone Dh5ZkjGD8EVujR7C8mxMyYaE2LRVarJ9W6bMofTgNJFP \ # Treasury - --clone HqPcY2CUB4FL5EAGWN1yZkS6DHYUoMsnjoSpdGqV8wPC \ # Treasury tokens - --clone oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp \ # ORE mint - --clone DvYP7L1dH6vK4CbEKtQehP9cSZC5qXFP53NQi5THt5U5 \ # Boost proof - --clone FQpx4mmybtZSwv8G5QwfyXWYjRt9nkqiw3sAsKGiUpCG \ # Boost config - --clone GgaDTFbqdgjoZz3FP7zrtofGwnRS4E6MCzmmD5Ni1Mxj \ # ORE-SOL pool - --clone 3s6ki6dQSM8FuqWiPsnGkgVsAEo8BTAfUR1Vvt1TPiJN \ # A vault - --clone FERjPVNEa7Udq8CEv68h6tPL46Tq7ieE49HrE2wea3XT \ # B vault - --clone BtJuiRG44vew5nYBVeUhuBawPTZLyYYxdzTYzerkfnto \ # A token vault - --clone HZeLxbZ9uHtSpwZC3LBr4Nubd14iHwz7bRSghRZf5VCG \ # B token vault - --clone 6Av9sdKvnjwoDHVnhEiz6JEq8e6SGzmhCsCncT2WJ7nN \ # A vault LP mint - --clone FZN7QZ8ZUUAxMPfxYEYkH3cXUASzH8EqA6B4tyCL8f1j \ # B vault LP mint - --clone 2k7V1NtM1krwh1sdt5wWqBRcvNQ5jzxj3J2rV78zdTsL \ # A vault LP - --clone CFATQFgkKXJyU3MdCNvQqN79qorNSMJFF8jrF66a7r6i \ # B vault LP - --clone 3WYz5TC8X4FLvwWQ2QvSfXuZHXjqvsdymKwmMFkgCgVs \ # Protocol token fee A - --clone 6kzYo2LMo2q2bkLAD8ienoG5NC1MkNXNTfm8sdyHuX3h # Protocol token fee B - diff --git a/localnet.sh b/localnet.sh index 8996acc..e5623f4 100755 --- a/localnet.sh +++ b/localnet.sh @@ -1,3 +1,23 @@ +# BoostzzkNfCA9D1qNuN5xZxB5ErbK4zQuBeTHGDpXT1 - Boost program +# Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB - Meteora pools program +# 24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi - Meteora vault program +# 2G3WKgP9Eemzgbzk5B4w2JSAizjWfEmEwa4JMfzLyXzM - ORE config +# Dh5ZkjGD8EVujR7C8mxMyYaE2LRVarJ9W6bMofTgNJFP - Treasury +# HqPcY2CUB4FL5EAGWN1yZkS6DHYUoMsnjoSpdGqV8wPC - Treasury tokens +# oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp - ORE mint +# DvYP7L1dH6vK4CbEKtQehP9cSZC5qXFP53NQi5THt5U5 - Boost proof +# FQpx4mmybtZSwv8G5QwfyXWYjRt9nkqiw3sAsKGiUpCG - Boost config +# GgaDTFbqdgjoZz3FP7zrtofGwnRS4E6MCzmmD5Ni1Mxj - ORE-SOL pool +# 3s6ki6dQSM8FuqWiPsnGkgVsAEo8BTAfUR1Vvt1TPiJN - A vault +# FERjPVNEa7Udq8CEv68h6tPL46Tq7ieE49HrE2wea3XT - B vault +# BtJuiRG44vew5nYBVeUhuBawPTZLyYYxdzTYzerkfnto - A token vault +# HZeLxbZ9uHtSpwZC3LBr4Nubd14iHwz7bRSghRZf5VCG - B token vault +# 6Av9sdKvnjwoDHVnhEiz6JEq8e6SGzmhCsCncT2WJ7nN - A vault LP mint +# FZN7QZ8ZUUAxMPfxYEYkH3cXUASzH8EqA6B4tyCL8f1j - B vault LP mint +# 2k7V1NtM1krwh1sdt5wWqBRcvNQ5jzxj3J2rV78zdTsL - A vault LP +# CFATQFgkKXJyU3MdCNvQqN79qorNSMJFF8jrF66a7r6i - B vault LP +# 3WYz5TC8X4FLvwWQ2QvSfXuZHXjqvsdymKwmMFkgCgVs - Protocol token fee A +# 6kzYo2LMo2q2bkLAD8ienoG5NC1MkNXNTfm8sdyHuX3h - Protocol token fee B solana-test-validator \ -r \ --bpf-program oreV2ZymfyeXgNgBdqMkumTqqAprVqgBWQfoYkrtKWQ target/deploy/ore.so \ diff --git a/program/src/bet.rs b/program/src/bet.rs index 3a8ba78..60e99e9 100644 --- a/program/src/bet.rs +++ b/program/src/bet.rs @@ -21,8 +21,12 @@ pub fn process_bet(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { .as_account_mut::(&ore_api::ID)? .assert_mut(|b| b.ends_at > clock.slot)? .assert_mut(|b| b.payed_out == 0)?; - block_bets_info.as_associated_token_account(block_info.key, &block.mint)?; - sender_info.as_associated_token_account(signer_info.key, &block.mint)?; + block_bets_info + .is_writable()? + .as_associated_token_account(block_info.key, &block.mint)?; + sender_info + .is_writable()? + .as_associated_token_account(signer_info.key, &block.mint)?; wager_info.is_writable()?.is_empty()?.has_seeds( &[WAGER, &block.current_round.to_le_bytes(), &seed], &ore_api::ID, @@ -37,11 +41,7 @@ pub fn process_bet(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { &system_program, &signer_info, &ore_api::ID, - &[ - WAGER, - &block.current_round.to_le_bytes(), - &block.bet_count.to_le_bytes(), - ], + &[WAGER, &block.current_round.to_le_bytes(), &seed], )?; let wager = wager_info.as_account_mut::(&ore_api::ID)?; wager.amount = amount; diff --git a/program/src/bury.rs b/program/src/bury.rs index 7c7ef65..ef29115 100644 --- a/program/src/bury.rs +++ b/program/src/bury.rs @@ -6,7 +6,7 @@ use steel::*; pub fn process_bury(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { // Load accounts. let clock = Clock::get()?; - let (required_accounts, meteora_accounts) = accounts.split_at(5); + let (required_accounts, meteora_accounts) = accounts.split_at(6); let [signer_info, block_info, block_bets_info, block_ore_info, bet_mint_info, ore_mint_info] = required_accounts else { diff --git a/program/src/reset.rs b/program/src/reset.rs index 7fbbbef..e840416 100644 --- a/program/src/reset.rs +++ b/program/src/reset.rs @@ -6,7 +6,7 @@ use steel::*; pub fn process_reset(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { // Load accounts. let clock = Clock::get()?; - let (required_accounts, boost_accounts) = accounts.split_at(5); + let (required_accounts, boost_accounts) = accounts.split_at(6); let [signer_info, block_info, mint_info, treasury_info, treasury_tokens_info, token_program] = required_accounts else { @@ -15,7 +15,7 @@ pub fn process_reset(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResul signer_info.is_signer()?; let block = block_info .as_account_mut::(&ore_api::ID)? - .assert_mut(|b| b.ends_at < clock.slot)? + .assert_mut(|b| b.ends_at <= clock.slot)? .assert_mut(|b| b.payed_out != 0)?; let mint = mint_info .has_address(&MINT_ADDRESS)?