This commit is contained in:
Hardhat Chad
2024-07-08 15:56:42 +00:00
7 changed files with 87 additions and 69 deletions

View File

@@ -31,20 +31,20 @@ pub const ONE_MINUTE: i64 = 60;
pub const EPOCH_MINUTES: i64 = 1;
/// The duration of a program epoch, in seconds.
pub const EPOCH_DURATION: i64 = ONE_MINUTE.saturating_mul(EPOCH_MINUTES);
pub const EPOCH_DURATION: i64 = ONE_MINUTE * EPOCH_MINUTES;
/// The maximum token supply (21 million).
pub const MAX_SUPPLY: u64 = ONE_ORE.saturating_mul(21_000_000);
pub const MAX_SUPPLY: u64 = ONE_ORE * 21_000_000;
/// The target quantity of ORE to be mined per epoch.
pub const TARGET_EPOCH_REWARDS: u64 = ONE_ORE.saturating_mul(EPOCH_MINUTES as u64);
pub const TARGET_EPOCH_REWARDS: u64 = ONE_ORE * EPOCH_MINUTES as u64;
/// The maximum quantity of ORE that can be mined per epoch.
/// Inflation rate ≈ 1 ORE / min (min 0, max 8)
pub const MAX_EPOCH_REWARDS: u64 = TARGET_EPOCH_REWARDS.saturating_mul(BUS_COUNT as u64);
pub const MAX_EPOCH_REWARDS: u64 = TARGET_EPOCH_REWARDS * BUS_COUNT as u64;
/// The quantity of ORE each bus is allowed to issue per epoch.
pub const BUS_EPOCH_REWARDS: u64 = MAX_EPOCH_REWARDS.saturating_div(BUS_COUNT as u64);
pub const BUS_EPOCH_REWARDS: u64 = MAX_EPOCH_REWARDS / BUS_COUNT as u64;
/// The number of bus accounts, for parallelizing mine operations.
pub const BUS_COUNT: usize = 8;

View File

@@ -23,6 +23,10 @@ pub enum OreError {
ToleranceOverflow = 7,
#[error("The maximum supply has been reached")]
MaxSupply = 8,
#[error("This account cannot be closed because it's the top staker")]
CannotClose = 9,
#[error("This account cannot be crowned because its last stake was too recent")]
CannotCrown = 10,
}
impl From<OreError> for ProgramError {

View File

@@ -289,9 +289,7 @@ pub fn load_mint<'a, 'info>(
return Err(ProgramError::UninitializedAccount);
}
if Mint::unpack_unchecked(&info.data.borrow()).is_err() {
return Err(ProgramError::InvalidAccountData);
}
Mint::unpack(&info.data.borrow())?;
if is_writable && !info.is_writable {
return Err(ProgramError::InvalidAccountData);
@@ -322,8 +320,7 @@ pub fn load_token_account<'a, 'info>(
}
let account_data = info.data.borrow();
let account = spl_token::state::Account::unpack_unchecked(&account_data)
.or(Err(ProgramError::InvalidAccountData))?;
let account = spl_token::state::Account::unpack(&account_data)?;
if account.mint.ne(&mint) {
return Err(ProgramError::InvalidAccountData);

View File

@@ -1,4 +1,8 @@
use ore_api::{loaders::*, state::Proof};
use ore_api::{
error::OreError,
loaders::*,
state::{Config, Proof},
};
use solana_program::{
account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError,
pubkey::Pubkey, system_program,
@@ -20,13 +24,21 @@ pub fn process_close<'a, 'info>(
_data: &[u8],
) -> ProgramResult {
// Load accounts
let [signer, proof_info, system_program] = accounts else {
let [signer, config_info, proof_info, system_program] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
load_signer(signer)?;
load_config(config_info, false)?;
load_proof(proof_info, signer.key, true)?;
load_program(system_program, system_program::id())?;
// Validate the account is not the crowned top staker.
let config_data = config_info.data.borrow();
let config = Config::try_from_bytes(&config_data)?;
if config.top_staker.eq(proof_info.key) {
return Err(OreError::CannotClose.into());
}
// Validate balance is zero
let proof_data = proof_info.data.borrow();
let proof = Proof::try_from_bytes(&proof_data)?;

View File

@@ -1,10 +1,12 @@
use ore_api::{
consts::ONE_MINUTE,
error::OreError,
loaders::*,
state::{Config, Proof},
};
use solana_program::{
account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError,
pubkey::Pubkey,
account_info::AccountInfo, clock::Clock, entrypoint::ProgramResult,
program_error::ProgramError, pubkey::Pubkey, sysvar::Sysvar,
};
use crate::utils::AccountDeserialize;
@@ -23,15 +25,23 @@ pub fn process_crown<'a, 'info>(
load_config(config_info, true)?;
load_any_proof(proof_new_info, false)?;
// Load config
let mut config_data = config_info.data.borrow_mut();
let config = Config::try_from_bytes_mut(&mut config_data)?;
// Load proposed new top staker
// Load the proof accounts.
let clock = Clock::get().unwrap();
let proof_new_data = proof_new_info.data.borrow();
let proof_new = Proof::try_from_bytes(&proof_new_data)?;
if proof_new
.last_stake_at
.saturating_add(ONE_MINUTE)
.gt(&clock.unix_timestamp)
{
return Err(OreError::CannotCrown.into());
}
// If top staker is the defualt null balance, skip this.
// If top staker is the default null address, skip this.
let mut config_data = config_info.data.borrow_mut();
let config = Config::try_from_bytes_mut(&mut config_data)?;
let proof_data = proof_info.data.borrow();
let proof = Proof::try_from_bytes(&proof_data)?;
if config.top_staker.ne(&Pubkey::new_from_array([0; 32])) {
// Load current top staker
load_any_proof(proof_info, false)?;
@@ -40,14 +50,12 @@ pub fn process_crown<'a, 'info>(
}
// Compare balances
let proof_data = proof_info.data.borrow();
let proof = Proof::try_from_bytes(&proof_data)?;
if proof_new.balance.lt(&proof.balance) {
return Ok(());
}
}
// Crown the new top staker
// Crown the new top staker.
config.max_stake = proof_new.balance;
config.top_staker = *proof_new_info.key;

View File

@@ -18,7 +18,6 @@ use solana_program::{
entrypoint::ProgramResult,
log::sol_log,
program_error::ProgramError,
pubkey,
pubkey::Pubkey,
sanitize::SanitizeError,
serialize_utils::{read_pubkey, read_u16, read_u8},
@@ -63,7 +62,7 @@ pub fn process_mine<'a, 'info>(
load_sysvar(slot_hashes_sysvar, sysvar::slot_hashes::id())?;
// Validate this is the only mine ix in the transaction.
if !validate_transaction(&instructions_sysvar.data.borrow()).unwrap_or(false) {
if !introspect_transaction(&instructions_sysvar.data.borrow()).unwrap_or(false) {
return Err(OreError::TransactionInvalid.into());
}
@@ -94,33 +93,28 @@ pub fn process_mine<'a, 'info>(
if difficulty.lt(&MIN_DIFFICULTY) {
return Err(OreError::HashTooEasy.into());
}
// Calculate base reward rate.
let difficulty = difficulty.saturating_sub(MIN_DIFFICULTY);
let mut reward = config
.base_reward_rate
.saturating_mul(2u64.saturating_pow(difficulty));
.checked_mul(2u64.checked_pow(difficulty).unwrap())
.unwrap();
// Apply staking multiplier.
// If user has greater than or equal to the max stake on the network, they receive 2x multiplier.
// Any stake less than this will receives between 1x and 2x multipler. The multipler is only active
// if the miner's last stake deposit was more than one minute ago.
if config.max_stake.gt(&0)
&& proof
.last_stake_at
.saturating_add(ONE_MINUTE)
.le(&clock.unix_timestamp)
{
let t = clock.unix_timestamp;
if config.max_stake.gt(&0) && proof.last_stake_at.saturating_add(ONE_MINUTE).le(&t) {
let staking_reward = proof
.balance
.min(config.max_stake)
.saturating_mul(reward)
.saturating_div(config.max_stake);
reward = reward.saturating_add(staking_reward);
.checked_mul(reward)
.unwrap()
.checked_div(config.max_stake)
.unwrap();
reward = reward.checked_add(staking_reward).unwrap();
}
// Reject spam transactions.
let t = clock.unix_timestamp;
let t_target = proof.last_hash_at.saturating_add(ONE_MINUTE);
let t_spam = t_target.saturating_sub(TOLERANCE);
if t.lt(&t_spam) {
@@ -130,11 +124,15 @@ pub fn process_mine<'a, 'info>(
// Apply liveness penalty.
let t_liveness = t_target.saturating_add(TOLERANCE);
if t.gt(&t_liveness) {
reward = reward.saturating_sub(
reward
.saturating_mul(t.saturating_sub(t_liveness) as u64)
.saturating_div(ONE_MINUTE as u64),
);
reward = reward
.checked_sub(
reward
.checked_mul(t.checked_sub(t_liveness).unwrap() as u64)
.unwrap()
.checked_div(ONE_MINUTE as u64)
.unwrap(),
)
.unwrap();
}
// Limit payout amount to whatever is left in the bus
@@ -143,9 +141,9 @@ pub fn process_mine<'a, 'info>(
let reward_actual = reward.min(bus.rewards);
// Update balances
bus.theoretical_rewards = bus.theoretical_rewards.saturating_add(reward);
bus.rewards = bus.rewards.saturating_sub(reward_actual);
proof.balance = proof.balance.saturating_add(reward_actual);
bus.theoretical_rewards = bus.theoretical_rewards.checked_add(reward).unwrap();
bus.rewards = bus.rewards.checked_sub(reward_actual).unwrap();
proof.balance = proof.balance.checked_add(reward_actual).unwrap();
// Hash recent slot hash into the next challenge to prevent pre-mining attacks
proof.last_hash = hash.h;
@@ -156,10 +154,7 @@ pub fn process_mine<'a, 'info>(
.0;
// Update time trackers
proof.last_hash_at = proof
.last_hash_at
.saturating_add(ONE_MINUTE)
.max(clock.unix_timestamp);
proof.last_hash_at = t.max(t_target);
// Update lifetime stats
proof.total_hashes = proof.total_hashes.saturating_add(1);
@@ -187,7 +182,7 @@ pub fn process_mine<'a, 'info>(
///
/// If each transaction is limited to one hash only, then a user will minimize their fee / hash
/// by allocating all their hashpower to finding the single most difficult hash they can.
fn validate_transaction(msg: &[u8]) -> Result<bool, SanitizeError> {
fn introspect_transaction(msg: &[u8]) -> Result<bool, SanitizeError> {
#[allow(deprecated)]
let idx = load_current_index(msg);
let mut c = 0;
@@ -198,27 +193,29 @@ fn validate_transaction(msg: &[u8]) -> Result<bool, SanitizeError> {
c = read_u16(&mut c, msg)? as usize;
let num_accounts = read_u16(&mut c, msg)? as usize;
c += num_accounts * 33;
// Only allow instructions to call ore and the compute budget program.
match read_pubkey(&mut c, msg)? {
ore_api::ID => {
c += 2;
if let Ok(ix) = OreInstruction::try_from(read_u8(&mut c, msg)?) {
if let OreInstruction::Mine = ix {
if i.ne(&(idx as usize)) {
return Ok(false);
}
}
} else {
let program_id = read_pubkey(&mut c, msg)?;
if i.eq(&(idx as usize)) {
// Require top-level instruction at current index is a `mine`
if program_id.ne(&ore_api::ID) {
return Ok(false);
}
if let Ok(ix) = OreInstruction::try_from(read_u8(&mut c, msg)?) {
if ix.ne(&OreInstruction::Mine) {
return Ok(false);
}
}
COMPUTE_BUDGET_PROGRAM_ID => {} // Noop
_ => return Ok(false),
} else {
// Require no other instructions in the transaction are a `mine`
if program_id.eq(&ore_api::ID) {
c += 2;
if let Ok(ix) = OreInstruction::try_from(read_u8(&mut c, msg)?) {
if ix.eq(&OreInstruction::Mine) {
return Ok(false);
}
}
}
}
}
Ok(true)
}
/// Program id of the compute budge program.
const COMPUTE_BUDGET_PROGRAM_ID: Pubkey = pubkey!("ComputeBudget111111111111111111111111111111");

View File

@@ -36,7 +36,7 @@ pub fn process_stake<'a, 'info>(
// Update proof balance
let mut proof_data = proof_info.data.borrow_mut();
let proof = Proof::try_from_bytes_mut(&mut proof_data)?;
proof.balance = proof.balance.saturating_add(amount);
proof.balance = proof.balance.checked_add(amount).unwrap();
// Update deposit timestamp
let clock = Clock::get().or(Err(ProgramError::InvalidAccountData))?;