This commit is contained in:
Hardhat Chad
2025-10-02 11:49:35 -07:00
parent c745ed19e9
commit eb40335fa3
16 changed files with 3798 additions and 247 deletions

View File

@@ -1,13 +1,12 @@
use ore_api::prelude::*;
use solana_program::rent::Rent;
use solana_program::{log::sol_log, rent::Rent};
use steel::*;
/// Checkpoints a miner's rewards.
pub fn process_checkpoint(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult {
// Load accounts.
let clock = Clock::get()?;
let [signer_info, automation_info, board_info, miner_info, round_info, treasury_info, system_program] =
accounts
let [signer_info, board_info, miner_info, round_info, treasury_info, system_program] = accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
@@ -40,37 +39,25 @@ pub fn process_checkpoint(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program
return Ok(());
}
// Calculate bot fee permissions.
// Get the RNG.
let Some(r) = round.rng() else {
// If the round has no RNG, no one wins.
miner.checkpoint_id = miner.round_id;
return Ok(());
};
// Calculate bot fee.
let mut bot_fee = 0;
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.
// Anyone may checkpoint this account and collect the bot fee.
bot_fee = miner.checkpoint_fee;
miner.checkpoint_fee = 0;
} else {
// 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
.as_account::<Automation>(&ore_api::ID)?
.assert(|a| a.authority == miner.authority)?;
assert!(
*signer_info.key == miner.authority || *signer_info.key == automation.executor,
"Only the miner or automation executor can checkpoint this account"
);
} else {
assert!(
*signer_info.key == miner.authority,
"Only the miner can checkpoint this account"
);
}
}
// Calculate miner rewards.
let mut rewards_sol = 0;
let mut rewards_ore = 0;
let r = round.rng();
let winning_square = round.winning_square(r) as usize;
if miner.deployed[winning_square] > 0 {
// Sanity check.
@@ -101,6 +88,8 @@ pub fn process_checkpoint(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program
}
}
sol_log("I");
// Checkpoint miner.
miner.checkpoint_id = round.id;
miner.rewards_ore += rewards_ore;
@@ -116,8 +105,9 @@ pub fn process_checkpoint(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program
miner_info.send(bot_fee, &signer_info);
}
// TODO Round debts. Track total checkpointed and/or num miner's checkpointed.
// Assert round has sufficient funds for rent + debts.
// TODO Debts
let account_size = 8 + std::mem::size_of::<Round>();
let required_rent = Rent::get()?.minimum_balance(account_size);
assert!(

View File

@@ -1,11 +1,14 @@
use ore_api::prelude::*;
use solana_program::rent::Rent;
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, board_info, rent_payer_info, round_info, system_program] = accounts else {
let [signer_info, board_info, rent_payer_info, round_info, treasury_info, system_program] =
accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
signer_info.is_signer()?;
@@ -16,8 +19,18 @@ pub fn process_close(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResul
.assert_mut(|r| r.id < board.round_id)?
.assert_mut(|r| r.expires_at < clock.slot)? // Ensure round has expired.
.assert_mut(|r| r.rent_payer == *rent_payer_info.key)?; // Ensure the rent payer is the correct one.
let treasury = treasury_info.as_account_mut::<Treasury>(&ore_api::ID)?;
system_program.is_program(&system_program::ID)?;
// Vault all unclaimed rewards.
let size = 8 + std::mem::size_of::<Round>();
let min_rent = Rent::get()?.minimum_balance(size);
let unclaimed_sol = round_info.lamports() - min_rent;
if unclaimed_sol > 0 {
round_info.send(unclaimed_sol, treasury_info);
treasury.balance += unclaimed_sol;
}
// Close the account.
round_info.close(rent_payer_info)?;

View File

@@ -2,6 +2,8 @@ use ore_api::prelude::*;
use solana_program::{keccak::hashv, log::sol_log, native_token::lamports_to_sol};
use steel::*;
use crate::AUTHORIZED_ACCOUNTS;
/// Deploys capital to prospect on a square.
pub fn process_deploy(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult {
// Parse data.
@@ -18,16 +20,25 @@ pub fn process_deploy(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResul
};
signer_info.is_signer()?;
authority_info.is_writable()?;
automation_info.is_writable()?;
automation_info
.is_writable()?
.has_seeds(&[AUTOMATION, &authority_info.key.to_bytes()], &ore_api::ID)?;
let board = board_info
.as_account_mut::<Board>(&ore_api::ID)?
.assert_mut(|b| clock.slot >= b.start_slot && clock.slot < b.end_slot)?;
let round = round_info
.as_account_mut::<Round>(&ore_api::ID)?
.assert_mut(|r| r.id == board.round_id)?;
miner_info.is_writable()?;
miner_info
.is_writable()?
.has_seeds(&[MINER, &authority_info.key.to_bytes()], &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
@@ -68,17 +79,6 @@ pub fn process_deploy(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResul
}
}
// Log
sol_log(
&format!(
"Round {}. Deploying {} SOL to {} squares",
round.id,
lamports_to_sol(amount),
squares.iter().filter(|&&s| s).count(),
)
.as_str(),
);
// Open miner account.
let miner = if miner_info.data_is_empty() {
create_program_account::<Miner>(
@@ -127,6 +127,7 @@ pub fn process_deploy(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResul
// Calculate all deployments.
let mut total_amount = 0;
let mut total_squares = 0;
for (square_id, &should_deploy) in squares.iter().enumerate() {
// Skip if square index is out of bounds.
if square_id > 24 {
@@ -154,8 +155,9 @@ pub fn process_deploy(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResul
round.total_deployed += amount;
round.count[square_id] += 1;
// Update total amount
// Update totals.
total_amount += amount;
total_squares += 1;
// Exit early if automation does not have enough balance for another square.
if let Some(automation) = &automation {
@@ -185,6 +187,17 @@ pub fn process_deploy(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResul
round_info.collect(total_amount, &signer_info)?;
}
// Log
sol_log(
&format!(
"Round #{}: deploying {} SOL to {} squares",
round.id,
lamports_to_sol(amount),
total_squares,
)
.as_str(),
);
Ok(())
}

View File

@@ -9,9 +9,6 @@ mod claim_yield;
mod close;
mod deploy;
mod deposit;
mod migrate_board;
mod migrate_miner;
// mod initialize;
mod log;
mod reset;
mod set_admin;
@@ -31,9 +28,6 @@ use claim_yield::*;
use close::*;
use deploy::*;
use deposit::*;
use migrate_board::*;
use migrate_miner::*;
// use initialize::*;
use log::*;
use reset::*;
use set_admin::*;
@@ -62,7 +56,6 @@ pub fn process_instruction(
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)?,
// Staker

View File

@@ -1,19 +0,0 @@
use ore_api::prelude::*;
use solana_program::slot_hashes::SlotHashes;
use steel::*;
/// Pays out the winners and block reward.
pub fn process_migrate_board(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult {
// Load accounts.
let clock = Clock::get()?;
let [signer_info, board_info, system_program] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
signer_info.is_signer()?;
let board = board_info.as_account_mut::<Board>(&ore_api::ID)?;
system_program.is_program(&system_program::ID)?;
// TODO Migrate miner account.
Ok(())
}

View File

@@ -1,20 +0,0 @@
use ore_api::prelude::*;
use solana_program::slot_hashes::SlotHashes;
use steel::*;
/// Pays out the winners and block reward.
pub fn process_migrate_miner(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult {
// Load accounts.
let clock = Clock::get()?;
let [signer_info, miner_info, system_program] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
signer_info.is_signer()?;
let miner = miner_info.as_account_mut::<Miner>(&ore_api::ID)?;
system_program.is_program(&system_program::ID)?;
// TODO Migrate miner account.
// TODO Move refund_sol into rewards_sol.
Ok(())
}

View File

@@ -34,15 +34,43 @@ pub fn process_reset(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResul
ore_program.is_program(&ore_api::ID)?;
slot_hashes_sysvar.is_sysvar(&sysvar::slot_hashes::ID)?;
// Open next round account.
create_program_account::<Round>(
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::<Round>(&ore_api::ID)?;
round_next.id = board.round_id + 1;
round_next.deployed = [0; 25];
round_next.slot_hash = [0; 32];
round_next.count = [0; 25];
round_next.expires_at = clock.slot + 150 + 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 = 0;
round_next.total_deployed = 0;
round_next.total_vaulted = 0;
round_next.total_winnings = 0;
// Sample slot hash.
let mut r = 0;
let (winning_square, square_deployed) =
if let Ok(slot_hash) = get_slot_hash(board.end_slot, slot_hashes_sysvar) {
round.slot_hash = slot_hash;
r = round.rng();
let winning_square = round.winning_square(r);
let square_deployed = round.deployed[winning_square];
(winning_square, square_deployed)
if let Some(rng) = round.rng() {
r = rng;
let winning_square = round.winning_square(r);
let square_deployed = round.deployed[winning_square];
(winning_square, square_deployed)
} else {
// Cannot get slot hash. No one wins.
round.slot_hash = [u8::MAX; 32];
(u64::MAX as usize, 0)
}
} else {
// Cannot get slot hash. No one wins.
round.slot_hash = [u8::MAX; 32];
@@ -54,8 +82,8 @@ pub fn process_reset(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResul
// No one won. Vault all deployed.
if square_deployed == 0 {
// Update board.
round.total_vaulted = round.total_deployed;
// Vault all deployed.
round.total_vaulted = round.total_deployed - total_admin_fee;
treasury.balance += round.total_deployed - total_admin_fee;
// Emit event.
@@ -78,9 +106,14 @@ pub fn process_reset(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResul
.to_bytes(),
)?;
// Update board
board.round_id += 1;
board.start_slot = clock.slot + 1;
board.end_slot = board.start_slot + 150;
// Do SOL transfers.
board_info.send(total_admin_fee, &fee_collector_info);
board_info.send(round.total_deployed - total_admin_fee, &treasury_info);
round_info.send(total_admin_fee, &fee_collector_info);
round_info.send(round.total_deployed - total_admin_fee, &treasury_info);
return Ok(());
}
@@ -153,15 +186,16 @@ pub fn process_reset(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResul
}
// Validate top miner.
// TODO Safety checks here.
let top_miner_sample = round.top_miner_sample(r, winning_square);
let top_miner = top_miner_info
.as_account::<Miner>(&ore_api::ID)?
.assert(|m| m.round_id == round.id)?
.assert(|m| {
m.cumulative[winning_square] >= top_miner_sample
&& top_miner_sample < m.cumulative[winning_square] + m.deployed[winning_square]
})?;
// TODO Safety checks here (if no one won).
// let mut top_miner_address = Pubkey::default();
// let top_miner_sample = round.top_miner_sample(r, winning_square);
// let top_miner = top_miner_info
// .as_account::<Miner>(&ore_api::ID)?
// .assert(|m| m.round_id == round.id)?
// .assert(|m| {
// m.cumulative[winning_square] >= top_miner_sample
// && top_miner_sample < m.cumulative[winning_square] + m.deployed[winning_square]
// })?;
// Emit event.
program_log(
@@ -172,7 +206,7 @@ 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: top_miner.authority,
top_miner: Pubkey::default(), // top_miner.authority,
num_winners: round.count[winning_square],
total_deployed: round.total_deployed,
total_vaulted: round.total_vaulted,
@@ -188,28 +222,6 @@ pub fn process_reset(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResul
board.start_slot = clock.slot + 1;
board.end_slot = board.start_slot + 150;
// Open next round account.
create_program_account::<Round>(
round_next_info,
ore_program,
signer_info,
&ore_api::ID,
&[ROUND, &board.round_id.to_le_bytes()],
)?;
let round_next = round_next_info.as_account_mut::<Round>(&ore_api::ID)?;
round_next.id = board.round_id;
round_next.deployed = [0; 25];
round_next.slot_hash = [0; 32];
round_next.count = [0; 25];
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);

View File

@@ -1,9 +1,9 @@
use solana_program::pubkey;
use steel::*;
pub const AUTHORIZED_ACCOUNTS: [Pubkey; 4] = [
pub const AUTHORIZED_ACCOUNTS: [Pubkey; 1] = [
pubkey!("pqspJ298ryBjazPAr95J9sULCVpZe3HbZTWkbC1zrkS"),
pubkey!("By5JFFueXCqeqLk5MzR8ZSwFxASz3SKWX2TVfT1LTFbX"),
pubkey!("5Nb2ibzu4bWrwis2vNVD4mJprt6KTchzW6wgbVWM2PkY"),
pubkey!("6tUUXB6LuTE1Pzpe6sP4mZL9CNA5XQYGWYbn1oqPpKeH"),
// pubkey!("By5JFFueXCqeqLk5MzR8ZSwFxASz3SKWX2TVfT1LTFbX"),
// pubkey!("5Nb2ibzu4bWrwis2vNVD4mJprt6KTchzW6wgbVWM2PkY"),
// pubkey!("6tUUXB6LuTE1Pzpe6sP4mZL9CNA5XQYGWYbn1oqPpKeH"),
];