From 3af8f3108952c7115146a4b7e89f2b4f0c33f4a8 Mon Sep 17 00:00:00 2001 From: Hardhat Chad Date: Thu, 7 Mar 2024 18:48:01 +0000 Subject: [PATCH] comments --- src/consts.rs | 1 - src/error.rs | 4 +-- src/lib.rs | 4 ++- src/loaders.rs | 49 ++++++++++++++++++++++++++++++ src/processor/claim.rs | 9 ++++++ src/processor/initialize.rs | 17 ++++++++++- src/processor/mine.rs | 18 +++++++++-- src/processor/register.rs | 9 ++++++ src/processor/reset.rs | 45 ++++++++++++++++++++------- src/processor/update_admin.rs | 6 ++++ src/processor/update_difficulty.rs | 6 ++++ src/state/bus.rs | 3 ++ src/state/hash.rs | 1 + src/state/proof.rs | 2 ++ src/state/treasury.rs | 6 ++-- tests/test_initialize.rs | 2 +- tests/test_mine.rs | 2 +- tests/test_reset.rs | 4 +-- tests/test_update_admin.rs | 2 +- tests/test_update_difficulty.rs | 2 +- 20 files changed, 165 insertions(+), 27 deletions(-) diff --git a/src/consts.rs b/src/consts.rs index 56bd28c..b6227dd 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -1,6 +1,5 @@ use solana_program::{keccak::Hash, pubkey, pubkey::Pubkey}; -// TODO Set this before deployment /// The unix timestamp after which mining is allowed. pub const START_AT: i64 = 0; diff --git a/src/error.rs b/src/error.rs index 7f9d9b8..c232367 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,8 +7,8 @@ use thiserror::Error; pub enum OreError { #[error("The epoch is still active and cannot be reset")] EpochActive = 0, - #[error("The epoch has expired and needs reset")] - EpochExpired = 1, + #[error("The epoch has ended and needs reset")] + EpochEnded = 1, #[error("The provided hash was invalid")] InvalidHash = 2, #[error("The provided hash does not satisfy the difficulty requirement")] diff --git a/src/lib.rs b/src/lib.rs index b80f9ab..c4e28d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,9 @@ use solana_program::{ program_error::ProgramError, pubkey::Pubkey, }; -// TODO Increase decimals? +// TODO Increase TOKEN_DECIMALS to 12? +// TODO Set START_AT before launch. +// TODO Set pubkey consts for derived mainnet pdas before lanch. declare_id!("oreoDL2qcXyBdaYTEfd7F5MFLY5PAqqDooPLZv1XdBP"); diff --git a/src/loaders.rs b/src/loaders.rs index 641fde2..b9a00af 100644 --- a/src/loaders.rs +++ b/src/loaders.rs @@ -10,6 +10,8 @@ use crate::{ BUS_ADDRESSES, BUS_COUNT, MINT_ADDRESS, TREASURY_ADDRESS, }; +/// Errors if: +/// - Account is not a signer. pub fn load_signer<'a, 'info>(info: &'a AccountInfo<'info>) -> Result<(), ProgramError> { if !info.is_signer { return Err(ProgramError::MissingRequiredSignature); @@ -18,6 +20,13 @@ pub fn load_signer<'a, 'info>(info: &'a AccountInfo<'info>) -> Result<(), Progra Ok(()) } +/// Errors if: +/// - Account is not owned by Ore program. +/// - Data is empty. +/// - Data cannot deserialize into a bus account. +/// - Bus ID is not in 0-7 range. +/// - Address is not in set of valid bus address. +/// - Expected to be writable, but is not. pub fn load_bus<'a, 'info>( info: &'a AccountInfo<'info>, is_writable: bool, @@ -48,6 +57,12 @@ pub fn load_bus<'a, 'info>( Ok(()) } +/// Errors if: +/// - Account is not owned by Ore program. +/// - Data is empty. +/// - Data cannot deserialize into a proof account. +/// - Proof authority does not match the expected address. +/// - Expected to be writable, but is not. pub fn load_proof<'a, 'info>( info: &'a AccountInfo<'info>, authority: &Pubkey, @@ -75,6 +90,12 @@ pub fn load_proof<'a, 'info>( Ok(()) } +/// Errors if: +/// - Account is not owned by Ore program. +/// - Data is empty. +/// - Data cannot deserialize into a treasury account. +/// - Address does not match the expected address. +/// - Expected to be writable, but is not. pub fn load_treasury<'a, 'info>( info: &'a AccountInfo<'info>, is_writable: bool, @@ -98,6 +119,12 @@ pub fn load_treasury<'a, 'info>( Ok(()) } +/// Errors if: +/// - Account is not owned by SPL token program. +/// - Data is empty. +/// - Data cannot deserialize into a mint account. +/// - Address does not match the expected mint address. +/// - Expected to be writable, but is not. pub fn load_mint<'a, 'info>( info: &'a AccountInfo<'info>, is_writable: bool, @@ -126,6 +153,13 @@ pub fn load_mint<'a, 'info>( Ok(()) } +/// Errors if: +/// - Account is not owned by SPL token program. +/// - Data is empty. +/// - Data cannot deserialize into a token account. +/// - Token account owner does not match the expected owner address. +/// - Token account mint does not match the expected mint address. +/// - Expected to be writable, but is not. pub fn load_token_account<'a, 'info>( info: &'a AccountInfo<'info>, owner: Option<&Pubkey>, @@ -161,6 +195,9 @@ pub fn load_token_account<'a, 'info>( Ok(()) } +/// Errors if: +/// - Address does not match PDA derived from provided seeds. +/// - Cannot load as an uninitialized account. pub fn load_uninitialized_pda<'a, 'info>( info: &'a AccountInfo<'info>, seeds: &[&[u8]], @@ -172,6 +209,10 @@ pub fn load_uninitialized_pda<'a, 'info>( load_uninitialized_account(info) } +/// Errors if: +/// - Account is not owned by the system program. +/// - Data is not empty. +/// - Account is not writable. pub fn load_uninitialized_account<'a, 'info>( info: &'a AccountInfo<'info>, ) -> Result<(), ProgramError> { @@ -189,6 +230,8 @@ pub fn load_uninitialized_account<'a, 'info>( Ok(()) } +/// Errors if: +/// - Account cannot load with the expected address. pub fn load_sysvar<'a, 'info>( info: &'a AccountInfo<'info>, key: Pubkey, @@ -196,6 +239,9 @@ pub fn load_sysvar<'a, 'info>( load_account(info, key, false) } +/// Errors if: +/// - Account does not match the expected value. +/// - Expected to be writable, but is not. pub fn load_account<'a, 'info>( info: &'a AccountInfo<'info>, key: Pubkey, @@ -212,6 +258,9 @@ pub fn load_account<'a, 'info>( Ok(()) } +/// Errors if: +/// - Address does not match the expected value. +/// - Account is not executable. pub fn load_program<'a, 'info>( info: &'a AccountInfo<'info>, key: Pubkey, diff --git a/src/processor/claim.rs b/src/processor/claim.rs index 296f708..b10ae95 100644 --- a/src/processor/claim.rs +++ b/src/processor/claim.rs @@ -12,6 +12,15 @@ use crate::{ TREASURY, }; +/// Claim distributes owed token rewards from the treasury to the miner. It has 4 responsibilies: +/// 1. Transfer tokens from the treasury to the miner. +/// 2. Decrement the miner's claimable rewards counter by an appropriate amount. +/// 3. Update the program's lifetime stats. +/// +/// Safety requirements: +/// - Claim is a permissionless instruction and can be called by any miner. +/// - Can only succeed if the claimed amount is less than or equal to the miner's claimable rewards. +/// - The provided beneficiary token account, mint, treasury, treasury token account, and token program must be valid. pub fn process_claim<'a, 'info>( _program_id: &Pubkey, accounts: &'a [AccountInfo<'info>], diff --git a/src/processor/initialize.rs b/src/processor/initialize.rs index 1ff475f..1b9a25b 100644 --- a/src/processor/initialize.rs +++ b/src/processor/initialize.rs @@ -21,6 +21,21 @@ use crate::{ TOKEN_DECIMALS, TREASURY, TREASURY_ADDRESS, }; +/// Initialize sets up the Ore program state. Has 4 responisbilities: +/// 1. Initializes the 8 bus accounts. +/// 2. Initializes the treasury account. +/// 3. Initializes the Ore mint account. +/// 4. Initializes the treasury token account. +/// 5. Sets the admin address as the signer. +/// +/// Safety requirements: +/// - Initialize is a permissionless instruction and can be called by anyone. +/// - Can only succeed once for the entire lifetime of the program. +/// - Can only succeed if all provided PDAs match their expected values. +/// - Can only succeed if provided system program, token program, associated token program, and rent sysvar are valid. +/// +/// Discussion +/// - The caller of this instruction is set as the admin of the program. pub fn process_initialize<'a, 'info>( _program_id: &Pubkey, accounts: &'a [AccountInfo<'info>], @@ -108,7 +123,7 @@ pub fn process_initialize<'a, 'info>( let treasury = Treasury::try_from_bytes_mut(&mut treasury_data)?; treasury.bump = args.treasury_bump as u64; treasury.admin = *signer.key; - treasury.epoch_start_at = 0; + treasury.last_reset_at = 0; treasury.difficulty = INITIAL_DIFFICULTY.into(); treasury.reward_rate = INITIAL_REWARD_RATE; treasury.total_claimed_rewards = 0; diff --git a/src/processor/mine.rs b/src/processor/mine.rs index 44c7df9..f3aa88c 100644 --- a/src/processor/mine.rs +++ b/src/processor/mine.rs @@ -22,6 +22,18 @@ use crate::{ EPOCH_DURATION, }; +/// Mine is the primary workhorse instruction of the Ore program. It has 4 responsibilities including: +/// 1. Verify the provided hash is valid. +/// 2. Increment the user's claimable rewards counter. +/// 3. Generate a new challenge for the miner. +/// 4. Update the miner's lifetime stats. +/// +/// Safety requirements: +/// - Mine is a permissionless instruction and can be called by any miner. +/// - Can only succeed if the last reset was less than 60 seconds ago. +/// - Can only succeed if the provided SHA3 hash and nonce are valid and satisfy the difficulty. +/// - The the provided proof account must be associated with the signer. +/// - The provided bus, treasury, and slot hash sysvar must be valid. pub fn process_mine<'a, 'info>( _program_id: &Pubkey, accounts: &'a [AccountInfo<'info>], @@ -44,9 +56,9 @@ pub fn process_mine<'a, 'info>( let clock = Clock::get().or(Err(ProgramError::InvalidAccountData))?; let treasury_data = treasury_info.data.borrow(); let treasury = Treasury::try_from_bytes(&treasury_data)?; - let epoch_end_at = treasury.epoch_start_at.saturating_add(EPOCH_DURATION); - if clock.unix_timestamp.ge(&epoch_end_at) { - return Err(OreError::EpochExpired.into()); + let threshold = treasury.last_reset_at.saturating_add(EPOCH_DURATION); + if clock.unix_timestamp.ge(&threshold) { + return Err(OreError::EpochEnded.into()); } // Validate provided hash diff --git a/src/processor/register.rs b/src/processor/register.rs index c36cef2..97418d3 100644 --- a/src/processor/register.rs +++ b/src/processor/register.rs @@ -14,6 +14,15 @@ use crate::{ PROOF, }; +/// Register generates a new hash chain for a prospective miner. It has 2 responsibilities: +/// 1. Initializes a new proof account. +/// 2. Generates an initial hash for the miner from the signer's key. +/// +/// Safety requirements: +/// - Register is a permissionless instruction and can be called by anyone. +/// - Can only succeed if the provided proof acount PDA is valid (associated with the signer). +/// - Can only succeed once per signer. +/// - The provided system program must be valid. pub fn process_register<'a, 'info>( _program_id: &Pubkey, accounts: &'a [AccountInfo<'info>], diff --git a/src/processor/reset.rs b/src/processor/reset.rs index 882ddcb..611c68a 100644 --- a/src/processor/reset.rs +++ b/src/processor/reset.rs @@ -12,6 +12,22 @@ use crate::{ TARGET_EPOCH_REWARDS, TREASURY, }; +/// Reset transitions the Ore program from one epoch to the next. It is the most complex instruction in the +/// Ore program and has three primary responsibilities including: +/// 1. Reset bus account rewards counters. +/// 2. Adjust the reward rate to stabilize inflation. +/// 3. Top up the treasury token account to backup claims. +/// +/// Safety requirements: +/// - Reset is a permissionless crank function and can be invoked by anyone. +/// - Can only succeed if more 60 seconds or more have passed since the last successful reset. +/// - The busses, mint, treasury, treasury token account, and token program must all be valid. +/// +/// Discussion: +/// - It is critical that `reset` can only be invoked once per 60 second period to ensure the supply growth rate +/// stays within the guaranteed bounds of 0 ≤ R ≤ 2 ORE/min. +/// - The reward rate is dynamically adjusted based on last epoch's actual reward rate (measured hashpower) to +/// target an average supply growth rate of 1 ORE/min. pub fn process_reset<'a, 'info>( _program_id: &Pubkey, accounts: &'a [AccountInfo<'info>], @@ -46,30 +62,32 @@ pub fn process_reset<'a, 'info>( bus_7_info, ]; - // Validate epoch has ended + // Validate at least 60 seconds have passed since last reset let clock = Clock::get().or(Err(ProgramError::InvalidAccountData))?; let mut treasury_data = treasury_info.data.borrow_mut(); let treasury = Treasury::try_from_bytes_mut(&mut treasury_data)?; - let epoch_end_at = treasury.epoch_start_at.saturating_add(EPOCH_DURATION); - if clock.unix_timestamp.lt(&epoch_end_at) { + let threshold = treasury.last_reset_at.saturating_add(EPOCH_DURATION); + if clock.unix_timestamp.lt(&threshold) { return Err(OreError::EpochActive.into()); } - // Reset busses - let mut total_bus_rewards = 0u64; + // Record current timestamp + treasury.last_reset_at = clock.unix_timestamp; + + // Reset bus accounts and calculate actual rewards mined since last reset + let mut total_remaining_rewards = 0u64; for i in 0..BUS_COUNT { let mut bus_data = busses[i].data.borrow_mut(); let bus = Bus::try_from_bytes_mut(&mut bus_data)?; - total_bus_rewards = total_bus_rewards.saturating_add(bus.rewards); + total_remaining_rewards = total_remaining_rewards.saturating_add(bus.rewards); bus.rewards = BUS_EPOCH_REWARDS; } + let total_epoch_rewards = MAX_EPOCH_REWARDS.saturating_sub(total_remaining_rewards); - // Update the reward rate for the next epoch - let total_epoch_rewards = MAX_EPOCH_REWARDS.saturating_sub(total_bus_rewards); + // Update reward rate for next epoch treasury.reward_rate = calculate_new_reward_rate(treasury.reward_rate, total_epoch_rewards); - treasury.epoch_start_at = clock.unix_timestamp; - // Top up treasury token account + // Fund treasury token account let treasury_bump = treasury.bump as u8; drop(treasury_data); solana_program::program::invoke_signed( @@ -93,8 +111,13 @@ pub fn process_reset<'a, 'info>( Ok(()) } +/// This function calculates what the new reward rate should be based on how many total rewards were mined in the prior epoch. +/// The math is largely identitical to that used by the Bitcoin network for updating the difficulty between each epoch. +/// new_rate = current_rate * (target_rewards / actual_rewards) +/// The new rate is then smoothed by a constant factor to avoid unexpectedly large fluctuations. +/// In Ore's case, the epochs are so short (60 seconds) that the smoothing factor of 2 has been chosen. pub(crate) fn calculate_new_reward_rate(current_rate: u64, epoch_rewards: u64) -> u64 { - // Avoid division by zero. Leave the reward rate unchanged. + // Avoid division by zero. Leave the reward rate unchanged, if detected. if epoch_rewards.eq(&0) { return current_rate; } diff --git a/src/processor/update_admin.rs b/src/processor/update_admin.rs index 044c9b7..7815e98 100644 --- a/src/processor/update_admin.rs +++ b/src/processor/update_admin.rs @@ -5,6 +5,12 @@ use solana_program::{ use crate::{instruction::UpdateAdminArgs, loaders::*, state::Treasury, utils::AccountDeserialize}; +/// UpdateAdmin updates the program's admin account. It has 1 responsibility: +/// 1. Update the treasury admin address. +/// +/// Safety requirements: +/// - Can only succeed if the signer is the current program admin. +/// - Can only succeed if the provided treasury is valid. pub fn process_update_admin<'a, 'info>( _program_id: &Pubkey, accounts: &'a [AccountInfo<'info>], diff --git a/src/processor/update_difficulty.rs b/src/processor/update_difficulty.rs index 791d68c..9921364 100644 --- a/src/processor/update_difficulty.rs +++ b/src/processor/update_difficulty.rs @@ -7,6 +7,12 @@ use crate::{ instruction::UpdateDifficultyArgs, loaders::*, state::Treasury, utils::AccountDeserialize, }; +/// UpdateDifficulty updates the program's global difficulty value. It has 1 responsibility: +/// 1. Update the difficulty. +/// +/// Safety requirements: +/// - Can only succeed if the signer is the current program admin. +/// - Can only succeed if the provided treasury is valid. pub fn process_update_difficulty<'a, 'info>( _program_id: &Pubkey, accounts: &'a [AccountInfo<'info>], diff --git a/src/state/bus.rs b/src/state/bus.rs index 5e4f5fa..849fe36 100644 --- a/src/state/bus.rs +++ b/src/state/bus.rs @@ -6,6 +6,9 @@ use crate::{ utils::{AccountDiscriminator, Discriminator}, }; +/// Bus accounts are responsible for distributing mining rewards. +/// There are 8 busses total to minimize write-lock contention and allow for parallel mine operations. +/// Every epoch, the bus account rewards counters are topped up to 0.25 ORE each (2 ORE split amongst 8 busses). #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, Pod, ShankAccount, Zeroable)] pub struct Bus { diff --git a/src/state/hash.rs b/src/state/hash.rs index 5ad342f..4c4f89c 100644 --- a/src/state/hash.rs +++ b/src/state/hash.rs @@ -5,6 +5,7 @@ use solana_program::keccak::{Hash as KeccakHash, HASH_BYTES}; use crate::impl_to_bytes; +/// Hash is an equivalent type to solana_program::keccak::Hash which supports bytemuck serialization. #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] pub struct Hash(pub [u8; HASH_BYTES]); diff --git a/src/state/proof.rs b/src/state/proof.rs index 746cfe6..f3c1cc2 100644 --- a/src/state/proof.rs +++ b/src/state/proof.rs @@ -8,6 +8,8 @@ use crate::{ utils::{AccountDiscriminator, Discriminator}, }; +/// Proof accounts track a miner's current hash, claimable rewards, and lifetime stats. +/// Every miner is allowed one proof account which is required by the program to mine or claim rewards. #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, Pod, ShankAccount, Zeroable)] pub struct Proof { diff --git a/src/state/treasury.rs b/src/state/treasury.rs index e0f26c6..fa8bde6 100644 --- a/src/state/treasury.rs +++ b/src/state/treasury.rs @@ -8,6 +8,8 @@ use crate::{ utils::{AccountDiscriminator, Discriminator}, }; +/// Treasury is a singleton account which manages all program wide variables. +/// It is the mint authority for the Ore token and also the authority of the program-owned token account. #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, Pod, ShankAccount, Zeroable)] pub struct Treasury { @@ -20,8 +22,8 @@ pub struct Treasury { /// The hash difficulty. pub difficulty: Hash, - /// The timestamp of the start of the current epoch. - pub epoch_start_at: i64, + /// The timestamp of the reset invocation. + pub last_reset_at: i64, /// The reward rate to payout to miners for submiting valid hashes. pub reward_rate: u64, diff --git a/tests/test_initialize.rs b/tests/test_initialize.rs index b78b822..1dba2b6 100644 --- a/tests/test_initialize.rs +++ b/tests/test_initialize.rs @@ -43,7 +43,7 @@ async fn test_initialize() { assert_eq!(treasury.bump as u8, treasury_pda.1); assert_eq!(treasury.admin, payer.pubkey()); assert_eq!(treasury.difficulty, INITIAL_DIFFICULTY.into()); - assert_eq!(treasury.epoch_start_at as u8, 0); + assert_eq!(treasury.last_reset_at as u8, 0); assert_eq!(treasury.reward_rate, INITIAL_REWARD_RATE); assert_eq!(treasury.total_claimed_rewards as u8, 0); diff --git a/tests/test_mine.rs b/tests/test_mine.rs index 452d53f..53666ca 100644 --- a/tests/test_mine.rs +++ b/tests/test_mine.rs @@ -184,7 +184,7 @@ async fn setup_program_test_env() -> (BanksClient, Keypair, solana_program::hash bump: treasury_pda.1 as u64, admin: admin_address, difficulty: KeccakHash::new_from_array([u8::MAX; 32]).into(), - epoch_start_at: 100, + last_reset_at: 100, reward_rate: INITIAL_REWARD_RATE, total_claimed_rewards: 0, } diff --git a/tests/test_reset.rs b/tests/test_reset.rs index dd97372..d69fc73 100644 --- a/tests/test_reset.rs +++ b/tests/test_reset.rs @@ -63,7 +63,7 @@ async fn test_reset() { Pubkey::from_str("AeNqnoLwFanMd3ig9WoMxQZVwQHtCtqKMMBsT1sTrvz6").unwrap() ); assert_eq!(treasury.difficulty, INITIAL_DIFFICULTY.into()); - assert_eq!(treasury.epoch_start_at as u8, 100); + assert_eq!(treasury.last_reset_at as u8, 100); assert_eq!(treasury.reward_rate, INITIAL_REWARD_RATE.saturating_div(2)); assert_eq!(treasury.total_claimed_rewards as u8, 0); @@ -134,7 +134,7 @@ async fn setup_program_test_env() -> (BanksClient, Keypair, Hash) { bump: treasury_pda.1 as u64, admin: admin_address, difficulty: INITIAL_DIFFICULTY.into(), - epoch_start_at: 0, + last_reset_at: 0, reward_rate: INITIAL_REWARD_RATE, total_claimed_rewards: 0, } diff --git a/tests/test_update_admin.rs b/tests/test_update_admin.rs index c070c09..bc2f511 100644 --- a/tests/test_update_admin.rs +++ b/tests/test_update_admin.rs @@ -34,7 +34,7 @@ async fn test_update_admin() { assert_eq!(treasury_.bump, treasury.bump); assert_eq!(treasury_.admin, new_admin); assert_eq!(treasury_.difficulty, treasury.difficulty); - assert_eq!(treasury_.epoch_start_at, treasury.epoch_start_at); + assert_eq!(treasury_.last_reset_at, treasury.last_reset_at); assert_eq!(treasury_.reward_rate, treasury.reward_rate); assert_eq!( treasury_.total_claimed_rewards, diff --git a/tests/test_update_difficulty.rs b/tests/test_update_difficulty.rs index 003a37b..99c3991 100644 --- a/tests/test_update_difficulty.rs +++ b/tests/test_update_difficulty.rs @@ -34,7 +34,7 @@ async fn test_update_difficulty() { assert_eq!(treasury_.bump, treasury.bump); assert_eq!(treasury_.admin, treasury.admin); assert_eq!(treasury_.difficulty, new_difficulty.into()); - assert_eq!(treasury_.epoch_start_at, treasury.epoch_start_at); + assert_eq!(treasury_.last_reset_at, treasury.last_reset_at); assert_eq!(treasury_.reward_rate, treasury.reward_rate); assert_eq!( treasury_.total_claimed_rewards,