Files
ore/program/src/deploy.rs
Hardhat Chad bc3516995c autoclose
2025-10-02 13:52:27 -07:00

228 lines
7.4 KiB
Rust

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.
let args = Deploy::try_from_bytes(data)?;
let mut amount = u64::from_le_bytes(args.amount);
let mask = u32::from_le_bytes(args.squares);
// Load accounts.
let clock = Clock::get()?;
let [signer_info, authority_info, automation_info, board_info, miner_info, round_info, system_program] =
accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
signer_info.is_signer()?;
authority_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()?
.has_seeds(&[MINER, &authority_info.key.to_bytes()], &ore_api::ID)?;
system_program.is_program(&system_program::ID)?;
// Wait until first deploy to start round.
if board.end_slot == u64::MAX {
board.start_slot = clock.slot;
board.end_slot = board.start_slot + 150;
round.expires_at = board.end_slot + ONE_WEEK_SLOTS;
}
// Check if signer is the automation executor.
let automation = if !automation_info.data_is_empty() {
let automation = automation_info
.as_account_mut::<Automation>(&ore_api::ID)?
.assert_mut(|a| a.executor == *signer_info.key)?
.assert_mut(|a| a.authority == *authority_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(), &round.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;
}
}
// Open miner account.
let miner = if miner_info.data_is_empty() {
create_program_account::<Miner>(
miner_info,
system_program,
signer_info,
&ore_api::ID,
&[MINER, &signer_info.key.to_bytes()],
)?;
let miner = miner_info.as_account_mut::<Miner>(&ore_api::ID)?;
miner.authority = *signer_info.key;
miner.deployed = [0; 25];
miner.cumulative = [0; 25];
miner.rewards_sol = 0;
miner.rewards_ore = 0;
miner.round_id = round.id;
miner.checkpoint_id = round.id;
miner.lifetime_rewards_sol = 0;
miner.lifetime_rewards_ore = 0;
miner
} else {
miner_info
.as_account_mut::<Miner>(&ore_api::ID)?
.assert_mut(|m| {
if let Some(automation) = &automation {
m.authority == automation.authority
} else {
m.authority == *signer_info.key
}
})?
};
// Check whitelist
if !AUTHORIZED_ACCOUNTS.contains(&miner.authority) {
sol_log(miner.authority.to_string().as_str());
return Err(trace("Not authorized", OreError::NotAuthorized.into()));
}
// Reset miner
if miner.round_id != round.id {
// Assert miner has checkpointed prior round.
assert!(
miner.checkpoint_id == miner.round_id,
"Miner has not checkpointed"
);
// Reset miner for new round.
miner.deployed = [0; 25];
miner.cumulative = round.deployed;
miner.round_id = round.id;
}
// 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 {
break;
}
// Skip if square is not deployed to.
if !should_deploy {
continue;
}
// Skip if miner already deployed to this square.
if miner.deployed[square_id] > 0 {
continue;
}
// Record cumulative amount.
miner.cumulative[square_id] = round.deployed[square_id];
// Update miner
miner.deployed[square_id] = amount;
// Update board
round.deployed[square_id] += amount;
round.total_deployed += amount;
round.count[square_id] += 1;
// 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 {
if total_amount + automation.fee + amount > automation.balance {
break;
}
}
}
// Top up checkpoint fee.
if miner.checkpoint_fee == 0 {
miner.checkpoint_fee = CHECKPOINT_FEE;
miner_info.collect(CHECKPOINT_FEE, &signer_info)?;
}
// Transfer SOL.
if let Some(automation) = automation {
automation.balance -= total_amount + automation.fee;
automation_info.send(total_amount, &round_info);
automation_info.send(automation.fee, &signer_info);
// Close automation if balance is less than what's required to deploy 1 square.
if automation.balance < automation.amount + automation.fee {
automation_info.close(authority_info)?;
}
} else {
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(())
}
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
}