From b5a50622a29d2b565eee9564fcebb3bff2360d7d Mon Sep 17 00:00:00 2001 From: Hardhat Chad Date: Fri, 23 May 2025 16:29:31 -0700 Subject: [PATCH] proof of wager --- Cargo.lock | 10 +-- Cargo.toml | 3 + api/src/consts.rs | 6 ++ api/src/instruction.rs | 35 +++------- api/src/lib.rs | 3 - api/src/loaders.rs | 36 ---------- api/src/sdk.rs | 135 +------------------------------------- api/src/state/block.rs | 36 ++++++++++ api/src/state/bus.rs | 25 ------- api/src/state/config.rs | 43 ------------ api/src/state/mod.rs | 32 +++++---- api/src/state/wager.rs | 27 ++++++++ program/src/bet.rs | 81 +++++++++++++++++++++++ program/src/claim.rs | 52 --------------- program/src/close.rs | 20 +++--- program/src/initialize.rs | 116 +++++++------------------------- program/src/lib.rs | 24 +++---- program/src/migrate.rs | 124 ---------------------------------- program/src/mine.rs | 44 ------------- program/src/open.rs | 48 -------------- program/src/payout.rs | 62 +++++++++++++++++ program/src/reset.rs | 133 ++++++++----------------------------- program/src/update.rs | 22 ------- 23 files changed, 312 insertions(+), 805 deletions(-) delete mode 100644 api/src/loaders.rs create mode 100644 api/src/state/block.rs delete mode 100644 api/src/state/bus.rs delete mode 100644 api/src/state/config.rs create mode 100644 api/src/state/wager.rs create mode 100644 program/src/bet.rs delete mode 100644 program/src/claim.rs delete mode 100644 program/src/migrate.rs delete mode 100644 program/src/mine.rs delete mode 100644 program/src/open.rs create mode 100644 program/src/payout.rs delete mode 100644 program/src/update.rs diff --git a/Cargo.lock b/Cargo.lock index 932d8e5..c9e68d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1246,9 +1246,9 @@ dependencies = [ [[package]] name = "ore-api" -version = "3.6.0-beta" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff73b3394583f3df7ce0f58e9ec66d890777c6cc6dd739de63f8ec826fd4baf" +checksum = "90933d4deaa73a1c73201d8a1aed50ce88211718370ceb0408ead479cb77e227" dependencies = [ "array-const-fn-init", "bytemuck", @@ -1283,16 +1283,16 @@ dependencies = [ [[package]] name = "ore-boost-api" -version = "4.0.0-beta" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0d09065544f61448a879ee9cc04e1c45a13ba76c38988fce83faa3f7aa31b1f" +checksum = "0b6c68dc61ee91b269520653f607038bb7f6ebf053385b0a896d4e272bc9f485" dependencies = [ "array-const-fn-init", "bytemuck", "const-crypto", "fixed", "num_enum", - "ore-api 3.6.0-beta", + "ore-api 3.6.0", "solana-program", "spl-associated-token-account", "spl-token 4.0.2", diff --git a/Cargo.toml b/Cargo.toml index 9372964..02bb5ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,11 +24,14 @@ num_enum = "0.7.2" ore-api = { path = "api" } ore-boost-api = "4.0.0-alpha" solana-program = "^2.1" +solana-client = "^2.1" +solana-sdk = "^2.1" spl-token = { version = "^4", features = ["no-entrypoint"] } spl-associated-token-account = { version = "^6", features = [ "no-entrypoint" ] } static_assertions = "1.1.0" steel = { features = ["spl"], version = "4.0" } thiserror = "1.0.57" +tokio = { version = "1.37.0", features = ["full"] } [patch.crates-io] diff --git a/api/src/consts.rs b/api/src/consts.rs index 786bf35..ad91315 100644 --- a/api/src/consts.rs +++ b/api/src/consts.rs @@ -52,6 +52,12 @@ pub const SMOOTHING_FACTOR: u64 = 2; /// The seed of the bus account PDA. pub const BUS: &[u8] = b"bus"; +/// The seed of the block account PDA. +pub const BLOCK: &[u8] = b"block"; + +/// The seed of the wager account PDA. +pub const WAGER: &[u8] = b"wager"; + /// The seed of the config account PDA. pub const CONFIG: &[u8] = b"config"; diff --git a/api/src/instruction.rs b/api/src/instruction.rs index 70fbfd0..cf590b2 100644 --- a/api/src/instruction.rs +++ b/api/src/instruction.rs @@ -4,59 +4,40 @@ use steel::*; #[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] pub enum OreInstruction { // User - Claim = 0, + Bet = 0, Close = 1, - Mine = 2, - Open = 3, - Reset = 4, - Update = 5, + Payout = 2, + Reset = 3, // Admin Initialize = 100, - Migrate = 101, } #[repr(C)] #[derive(Clone, Copy, Debug, Pod, Zeroable)] -pub struct Claim { +pub struct Bet { pub amount: [u8; 8], + pub seed: [u8; 32], } #[repr(C)] #[derive(Clone, Copy, Debug, Pod, Zeroable)] pub struct Close {} -#[repr(C)] -#[derive(Clone, Copy, Debug, Pod, Zeroable)] -pub struct Mine { - pub nonce: [u8; 8], -} - -#[repr(C)] -#[derive(Clone, Copy, Debug, Pod, Zeroable)] -pub struct Open {} - #[repr(C)] #[derive(Clone, Copy, Debug, Pod, Zeroable)] pub struct Reset {} #[repr(C)] #[derive(Clone, Copy, Debug, Pod, Zeroable)] -pub struct Update {} +pub struct Payout {} #[repr(C)] #[derive(Clone, Copy, Debug, Pod, Zeroable)] pub struct Initialize {} -#[repr(C)] -#[derive(Clone, Copy, Debug, Pod, Zeroable)] -pub struct Migrate {} - -instruction!(OreInstruction, Claim); +instruction!(OreInstruction, Bet); instruction!(OreInstruction, Close); -instruction!(OreInstruction, Mine); -instruction!(OreInstruction, Open); +instruction!(OreInstruction, Payout); instruction!(OreInstruction, Reset); -instruction!(OreInstruction, Update); instruction!(OreInstruction, Initialize); -instruction!(OreInstruction, Migrate); diff --git a/api/src/lib.rs b/api/src/lib.rs index c0e78a6..7c83341 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -2,7 +2,6 @@ pub mod consts; pub mod error; pub mod event; pub mod instruction; -pub mod loaders; pub mod sdk; pub mod state; @@ -11,8 +10,6 @@ pub mod prelude { pub use crate::error::*; pub use crate::event::*; pub use crate::instruction::*; - pub use crate::loaders::*; - pub use crate::sdk::*; pub use crate::state::*; } diff --git a/api/src/loaders.rs b/api/src/loaders.rs deleted file mode 100644 index de380e2..0000000 --- a/api/src/loaders.rs +++ /dev/null @@ -1,36 +0,0 @@ -use steel::*; - -use crate::{ - consts::*, - state::{Config, Treasury}, -}; - -pub trait OreAccountInfoValidation { - fn is_bus(&self) -> Result<&Self, ProgramError>; - fn is_config(&self) -> Result<&Self, ProgramError>; - fn is_treasury(&self) -> Result<&Self, ProgramError>; - fn is_treasury_tokens(&self) -> Result<&Self, ProgramError>; -} - -impl OreAccountInfoValidation for AccountInfo<'_> { - fn is_bus(&self) -> Result<&Self, ProgramError> { - if !BUS_ADDRESSES.contains(self.key) { - return Err(ProgramError::InvalidSeeds); - } - Ok(self) - } - - fn is_config(&self) -> Result<&Self, ProgramError> { - self.has_address(&CONFIG_ADDRESS)? - .is_type::(&crate::ID) - } - - fn is_treasury(&self) -> Result<&Self, ProgramError> { - self.has_address(&TREASURY_ADDRESS)? - .is_type::(&crate::ID) - } - - fn is_treasury_tokens(&self) -> Result<&Self, ProgramError> { - self.has_address(&TREASURY_TOKENS_ADDRESS) - } -} diff --git a/api/src/sdk.rs b/api/src/sdk.rs index ef00b2e..b44e26a 100644 --- a/api/src/sdk.rs +++ b/api/src/sdk.rs @@ -1,136 +1,3 @@ use steel::*; -use crate::{consts::*, instruction::*, state::proof_pda}; - -/// Builds a claim instruction. -pub fn claim(signer: Pubkey, beneficiary: Pubkey, amount: u64) -> Instruction { - let proof = proof_pda(signer).0; - Instruction { - program_id: crate::ID, - accounts: vec![ - AccountMeta::new(signer, true), - AccountMeta::new(beneficiary, false), - AccountMeta::new(proof, false), - AccountMeta::new_readonly(TREASURY_ADDRESS, false), - AccountMeta::new(TREASURY_TOKENS_ADDRESS, false), - AccountMeta::new_readonly(spl_token::ID, false), - ], - data: Claim { - amount: amount.to_le_bytes(), - } - .to_bytes(), - } -} - -/// Builds a close instruction. -pub fn close(signer: Pubkey) -> Instruction { - let proof = proof_pda(signer).0; - Instruction { - program_id: crate::ID, - accounts: vec![ - AccountMeta::new(signer, true), - AccountMeta::new(proof, false), - AccountMeta::new_readonly(solana_program::system_program::ID, false), - ], - data: Close {}.to_bytes(), - } -} - -/// Builds a mine instruction. -pub fn mine( - signer: Pubkey, - authority: Pubkey, - bus: Pubkey, - nonce: u64, - boost_config: Pubkey, -) -> Instruction { - let proof = proof_pda(authority).0; - let accounts = vec![ - AccountMeta::new(signer, true), - AccountMeta::new(bus, false), - AccountMeta::new_readonly(CONFIG_ADDRESS, false), - AccountMeta::new(proof, false), - AccountMeta::new_readonly(sysvar::instructions::ID, false), - AccountMeta::new_readonly(sysvar::slot_hashes::ID, false), - AccountMeta::new_readonly(boost_config, false), - AccountMeta::new(proof_pda(boost_config).0, false), - ]; - Instruction { - program_id: crate::ID, - accounts, - data: Mine { - // digest: solution.d, - nonce: nonce.to_le_bytes(), - } - .to_bytes(), - } -} - -/// Builds an open instruction. -pub fn open(signer: Pubkey, miner: Pubkey, payer: Pubkey) -> Instruction { - let proof_pda = proof_pda(signer); - Instruction { - program_id: crate::ID, - accounts: vec![ - AccountMeta::new(signer, true), - AccountMeta::new_readonly(miner, false), - AccountMeta::new(payer, true), - AccountMeta::new(proof_pda.0, false), - AccountMeta::new_readonly(solana_program::system_program::ID, false), - AccountMeta::new_readonly(sysvar::slot_hashes::ID, false), - ], - data: Open {}.to_bytes(), - } -} - -/// Builds a reset instruction. -pub fn reset(signer: Pubkey, best_proof: Pubkey) -> Instruction { - Instruction { - program_id: crate::ID, - accounts: vec![ - AccountMeta::new(signer, true), - AccountMeta::new(CONFIG_ADDRESS, false), - AccountMeta::new(MINT_ADDRESS, false), - AccountMeta::new(best_proof, false), - AccountMeta::new(TREASURY_ADDRESS, false), - AccountMeta::new(TREASURY_TOKENS_ADDRESS, false), - AccountMeta::new_readonly(spl_token::ID, false), - ], - data: Reset {}.to_bytes(), - } -} - -// Build an update instruction. -pub fn update(signer: Pubkey, miner: Pubkey) -> Instruction { - let proof = proof_pda(signer).0; - Instruction { - program_id: crate::ID, - accounts: vec![ - AccountMeta::new(signer, true), - AccountMeta::new_readonly(miner, false), - AccountMeta::new(proof, false), - ], - data: Update {}.to_bytes(), - } -} - -// Builds an initialize instruction. -pub fn initialize(signer: Pubkey) -> Instruction { - Instruction { - program_id: crate::ID, - accounts: vec![ - AccountMeta::new(signer, true), - AccountMeta::new(CONFIG_ADDRESS, false), - AccountMeta::new(METADATA_ADDRESS, false), - AccountMeta::new(MINT_ADDRESS, false), - AccountMeta::new(TREASURY_ADDRESS, false), - AccountMeta::new(TREASURY_TOKENS_ADDRESS, false), - AccountMeta::new_readonly(system_program::ID, false), - AccountMeta::new_readonly(spl_token::ID, false), - AccountMeta::new_readonly(spl_associated_token_account::ID, false), - AccountMeta::new_readonly(mpl_token_metadata::ID, false), - AccountMeta::new_readonly(sysvar::rent::ID, false), - ], - data: Initialize {}.to_bytes(), - } -} +use crate::{consts::*, instruction::*, state::*}; diff --git a/api/src/state/block.rs b/api/src/state/block.rs new file mode 100644 index 0000000..055e2cf --- /dev/null +++ b/api/src/state/block.rs @@ -0,0 +1,36 @@ +use steel::*; + +use super::OreAccount; + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +pub struct Block { + /// The current round. + pub current_round: u64, + + /// The cumulative amount of SOL risked in the current round, up to and including this bet. + pub total_bets: u64, + + /// The number of bets made in the current round. + pub bet_count: u64, + + /// The time time the current round started. + pub started_at: u64, + + /// The slot at which the current round ends. + pub ends_at: u64, + + /// Whether or not the current round has ended. + pub payed_out: u64, + + /// The mint used to track wagers of the current round. + pub mint: Pubkey, + + /// The amount of ORE to distribute to the winner. + pub reward: u64, + + /// The noise used for the current round. + pub noise: [u8; 32], +} + +account!(OreAccount, Block); diff --git a/api/src/state/bus.rs b/api/src/state/bus.rs deleted file mode 100644 index 71504b3..0000000 --- a/api/src/state/bus.rs +++ /dev/null @@ -1,25 +0,0 @@ -use steel::*; - -use super::OreAccount; - -/// Bus accounts are responsible for distributing mining rewards. There are 8 busses total -/// to minimize write-lock contention and allow Solana to process mine instructions in parallel. -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -pub struct Bus { - /// The ID of the bus account. - pub id: u64, - - /// The remaining rewards this bus has left to payout in the current epoch. - pub rewards: u64, - - /// The rewards this bus would have paid out in the current epoch if there no limit. - /// This is used to calculate the updated reward rate. - pub theoretical_rewards: u64, - - /// The largest known stake balance seen by the bus this epoch. - #[deprecated(since = "2.8.0", note = "Top balance is no longer tracked or used")] - pub top_balance: u64, -} - -account!(OreAccount, Bus); diff --git a/api/src/state/config.rs b/api/src/state/config.rs deleted file mode 100644 index 08ff279..0000000 --- a/api/src/state/config.rs +++ /dev/null @@ -1,43 +0,0 @@ -use steel::*; - -use super::{OldOreAccount, OreAccount}; - -/// Config is a singleton account which manages program global variables. -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -pub struct Config { - /// The timestamp of the last reset. - pub last_reset_at: i64, - - /// The best difficulty score of this epoch. - pub best_hash: [u8; 32], - - /// The proof of the best submitted hash of this epoch. - pub best_proof: Pubkey, - - /// The challenge of this epoch. - pub challenge: [u8; 32], - - /// The target emissions rate in ORE/min. - pub block_reward: u64, -} - -/// Config is a singleton account which manages program global variables. -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -pub struct OldConfig { - /// The base reward rate paid out for a hash of minimum difficulty. - pub base_reward_rate: u64, - - /// The timestamp of the last reset. - pub last_reset_at: i64, - - /// The minimum accepted difficulty. - pub min_difficulty: u64, - - /// The target emissions rate in ORE/min. - pub target_emmissions_rate: u64, -} - -account!(OreAccount, Config); -account!(OldOreAccount, OldConfig); diff --git a/api/src/state/mod.rs b/api/src/state/mod.rs index 489760a..e0ffd2c 100644 --- a/api/src/state/mod.rs +++ b/api/src/state/mod.rs @@ -1,12 +1,12 @@ -mod bus; -mod config; +mod block; mod proof; mod treasury; +mod wager; -pub use bus::*; -pub use config::*; +pub use block::*; pub use proof::*; pub use treasury::*; +pub use wager::*; use steel::*; @@ -15,29 +15,27 @@ use crate::consts::*; #[repr(u8)] #[derive(Clone, Copy, Debug, Eq, PartialEq, IntoPrimitive, TryFromPrimitive)] pub enum OreAccount { - Bus = 100, - Config = 101, Proof = 102, Treasury = 103, + Block = 104, + Wager = 105, } -/// Derive the PDA of the config account. -pub fn config_pda() -> (Pubkey, u8) { - Pubkey::find_program_address(&[CONFIG], &crate::id()) +pub fn block_pda() -> (Pubkey, u8) { + Pubkey::find_program_address(&[BLOCK], &crate::ID) } -/// Derive the PDA of a proof account. pub fn proof_pda(authority: Pubkey) -> (Pubkey, u8) { Pubkey::find_program_address(&[PROOF, authority.as_ref()], &crate::id()) } -/// Derive the PDA of the treasury account. -pub fn treasury_pda() -> (Pubkey, u8) { - Pubkey::find_program_address(&[TREASURY], &crate::id()) +pub fn wager_pda(round: u64, id: u64) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[WAGER, &round.to_le_bytes(), &id.to_le_bytes()], + &crate::ID, + ) } -#[repr(u8)] -#[derive(Clone, Copy, Debug, Eq, PartialEq, IntoPrimitive, TryFromPrimitive)] -pub enum OldOreAccount { - OldConfig = 101, +pub fn treasury_pda() -> (Pubkey, u8) { + Pubkey::find_program_address(&[TREASURY], &crate::ID) } diff --git a/api/src/state/wager.rs b/api/src/state/wager.rs new file mode 100644 index 0000000..682fba3 --- /dev/null +++ b/api/src/state/wager.rs @@ -0,0 +1,27 @@ +use steel::*; + +use super::OreAccount; + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +pub struct Wager { + /// The signer authorized to use this wager. + pub authority: Pubkey, + + /// The current round this miner is betting in. + pub round: u64, + + /// The ID of the bet. + pub id: u64, + + /// The quantity of SOL this miner has bet in the current round. + pub amount: u64, + + /// The cumulative amount of SOL bet in the current round, up to and including this wager. + pub cumulative_bets: u64, + + /// The timestamp of the wager. + pub timestamp: u64, +} + +account!(OreAccount, Wager); diff --git a/program/src/bet.rs b/program/src/bet.rs new file mode 100644 index 0000000..3b303c3 --- /dev/null +++ b/program/src/bet.rs @@ -0,0 +1,81 @@ +use std::mem::size_of; + +use ore_api::prelude::*; +use solana_program::{keccak::hashv, slot_hashes::SlotHash}; +use steel::*; + +pub fn process_bet(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { + // Parse data. + let args = Bet::try_from_bytes(data)?; + let amount = u64::from_le_bytes(args.amount); + + // Load accounts. + let clock = Clock::get()?; + let [signer_info, block_info, wager_info, block_bets_info, sender_info, system_program, token_program, slot_hashes_sysvar] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + 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.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)?; + wager_info.is_writable()?.is_empty()?.has_seeds( + &[ + WAGER, + &block.current_round.to_le_bytes(), + &block.bet_count.to_le_bytes(), + ], + &ore_api::ID, + )?; + system_program.is_program(&system_program::ID)?; + token_program.is_program(&spl_token::ID)?; + slot_hashes_sysvar.is_sysvar(&sysvar::slot_hashes::ID)?; + + // Create wager account. + create_program_account::( + &wager_info, + &system_program, + &signer_info, + &ore_api::ID, + &[ + WAGER, + &block.current_round.to_le_bytes(), + &block.bet_count.to_le_bytes(), + ], + )?; + let wager = wager_info.as_account_mut::(&ore_api::ID)?; + wager.amount = amount; + wager.authority = *signer_info.key; + wager.id = block.bet_count; + wager.round = block.current_round; + wager.timestamp = clock.unix_timestamp as u64; + wager.cumulative_bets = block.total_bets; + + // Update block. + block.total_bets += amount; + block.bet_count += 1; + + // Hash client seed into block noise. Use a recent slot hash if no seed is provided. + // This follows the scheme for provable randomness. + let seed: &[u8] = if args.seed == [0; 32] { + &slot_hashes_sysvar.data.borrow()[0..size_of::()] + } else { + args.seed.as_slice() + }; + block.noise = hashv(&[&block.noise, seed]).to_bytes(); + + // Transfer wagers. + transfer( + &signer_info, + &sender_info, + &block_bets_info, + &token_program, + amount, + )?; + + Ok(()) +} diff --git a/program/src/claim.rs b/program/src/claim.rs deleted file mode 100644 index 29965f8..0000000 --- a/program/src/claim.rs +++ /dev/null @@ -1,52 +0,0 @@ -use ore_api::prelude::*; -use steel::*; - -/// Claim distributes claimable ORE from the treasury to a miner. -pub fn process_claim(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { - // Parse args. - let args = Claim::try_from_bytes(data)?; - let amount = u64::from_le_bytes(args.amount); - - // Load accounts. - let clock = Clock::get()?; - let [signer_info, beneficiary_info, proof_info, treasury_info, treasury_tokens_info, token_program] = - accounts - else { - return Err(ProgramError::NotEnoughAccountKeys); - }; - signer_info.is_signer()?; - beneficiary_info - .is_writable()? - .as_token_account()? - .assert(|t| t.mint() == MINT_ADDRESS)?; - let proof = proof_info - .as_account_mut::(&ore_api::ID)? - .assert_mut_err( - |p| p.authority == *signer_info.key, - ProgramError::MissingRequiredSignature, - )?; - treasury_info.is_treasury()?; - treasury_tokens_info.is_writable()?.is_treasury_tokens()?; - token_program.is_program(&spl_token::ID)?; - - // Update miner balance. - proof.balance = proof - .balance - .checked_sub(amount) - .ok_or(OreError::ClaimTooLarge)?; - - // Update last claim timestamp. - proof.last_claim_at = clock.unix_timestamp; - - // Transfer tokens from treasury to beneficiary. - transfer_signed( - treasury_info, - treasury_tokens_info, - beneficiary_info, - token_program, - amount, - &[TREASURY], - )?; - - Ok(()) -} diff --git a/program/src/close.rs b/program/src/close.rs index 326aca3..fa1b754 100644 --- a/program/src/close.rs +++ b/program/src/close.rs @@ -1,25 +1,21 @@ use ore_api::prelude::*; use steel::*; -/// Close closes a proof account and returns the rent to the owner. pub fn process_close(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { // Load accounts. - let [signer_info, proof_info, system_program] = accounts else { + let [signer_info, block_info, wager_info, system_program] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; signer_info.is_signer()?; - proof_info - .is_writable()? - .as_account::(&ore_api::ID)? - .assert_err( - |p| p.authority == *signer_info.key, - ProgramError::MissingRequiredSignature, - )? - .assert(|p| p.balance == 0)?; + let block = block_info.as_account::(&ore_api::ID)?; + wager_info + .as_account_mut::(&ore_api::ID)? + .assert_mut(|w| w.authority == *signer_info.key)? + .assert_mut(|w| w.round < block.current_round)?; system_program.is_program(&system_program::ID)?; - // Return rent to signer. - proof_info.close(signer_info)?; + // Close the wager account + wager_info.close(&signer_info)?; Ok(()) } diff --git a/program/src/initialize.rs b/program/src/initialize.rs index 1d0ef3a..c541ddd 100644 --- a/program/src/initialize.rs +++ b/program/src/initialize.rs @@ -1,122 +1,52 @@ use ore_api::prelude::*; -use solana_program::program_pack::Pack; -use spl_token::state::Mint; use steel::*; /// Initialize sets up the ORE program to begin mining. pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { // Load accounts. - let [signer_info, config_info, metadata_info, mint_info, treasury_info, treasury_tokens_info, system_program, token_program, associated_token_program, metadata_program, rent_sysvar] = + let [signer_info, block_info, block_bets_info, sol_mint_info, system_program, token_program, associated_token_program] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; signer_info.is_signer()?.has_address(&INITIALIZER_ADDRESS)?; - config_info + block_info .is_empty()? .is_writable()? - .has_seeds(&[CONFIG], &ore_api::ID)?; - metadata_info.is_empty()?.is_writable()?.has_seeds( - &[ - METADATA, - mpl_token_metadata::ID.as_ref(), - MINT_ADDRESS.as_ref(), - ], - &mpl_token_metadata::ID, - )?; - mint_info - .is_empty()? - .is_writable()? - .has_seeds(&[MINT, MINT_NOISE.as_slice()], &ore_api::ID)?; - treasury_info - .is_empty()? - .is_writable()? - .has_seeds(&[TREASURY], &ore_api::ID)?; - treasury_tokens_info.is_empty()?.is_writable()?; + .has_seeds(&[BLOCK], &ore_api::ID)?; + block_bets_info.is_empty()?.is_writable()?; + sol_mint_info + .has_address(&spl_token::native_mint::ID)? + .as_mint()?; system_program.is_program(&system_program::ID)?; token_program.is_program(&spl_token::ID)?; associated_token_program.is_program(&spl_associated_token_account::ID)?; - metadata_program.is_program(&mpl_token_metadata::ID)?; - rent_sysvar.is_sysvar(&sysvar::rent::ID)?; // Initialize config. - create_program_account::( - config_info, + create_program_account::( + block_info, system_program, signer_info, &ore_api::ID, - &[CONFIG], + &[BLOCK], )?; - let config = config_info.as_account_mut::(&ore_api::ID)?; - // config.base_reward_rate = INITIAL_BASE_REWARD_RATE; - config.last_reset_at = 0; - config.best_hash = [u8::MAX; 32]; - config.best_proof = Pubkey::default(); - config.challenge = [0; 32]; - config.block_reward = 0; - - // Initialize treasury. - create_program_account::( - treasury_info, - system_program, - signer_info, - &ore_api::ID, - &[TREASURY], - )?; - - // Initialize mint. - allocate_account_with_bump( - mint_info, - system_program, - signer_info, - Mint::LEN, - &spl_token::ID, - &[MINT, MINT_NOISE.as_slice()], - MINT_BUMP, - )?; - initialize_mint_signed_with_bump( - mint_info, - treasury_info, - None, - token_program, - rent_sysvar, - TOKEN_DECIMALS, - &[MINT, MINT_NOISE.as_slice()], - MINT_BUMP, - )?; - - // Initialize mint metadata. - mpl_token_metadata::instructions::CreateMetadataAccountV3Cpi { - __program: metadata_program, - metadata: metadata_info, - mint: mint_info, - mint_authority: treasury_info, - payer: signer_info, - update_authority: (signer_info, true), - system_program, - rent: Some(rent_sysvar), - __args: mpl_token_metadata::instructions::CreateMetadataAccountV3InstructionArgs { - data: mpl_token_metadata::types::DataV2 { - name: METADATA_NAME.to_string(), - symbol: METADATA_SYMBOL.to_string(), - uri: METADATA_URI.to_string(), - seller_fee_basis_points: 0, - creators: None, - collection: None, - uses: None, - }, - is_mutable: true, - collection_details: None, - }, - } - .invoke_signed(&[&[TREASURY, &[TREASURY_BUMP]]])?; + let block = block_info.as_account_mut::(&ore_api::ID)?; + block.current_round = 0; + block.total_bets = 0; + block.bet_count = 0; + block.started_at = 0; + block.ends_at = 0; + block.payed_out = 0; + block.mint = spl_token::native_mint::ID; + block.reward = 0; + block.noise = [0; 32]; // Initialize treasury token account. create_associated_token_account( signer_info, - treasury_info, - treasury_tokens_info, - mint_info, + block_info, + block_bets_info, + sol_mint_info, system_program, token_program, associated_token_program, diff --git a/program/src/lib.rs b/program/src/lib.rs index 15aaa3d..4a594ae 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -1,22 +1,17 @@ -mod claim; +mod bet; mod close; mod initialize; -mod migrate; -mod mine; -mod open; +mod payout; mod reset; -mod update; -use claim::*; +use bet::*; use close::*; use initialize::*; -use migrate::*; -use mine::*; -use open::*; -use ore_api::instruction::*; +use payout::*; use reset::*; + +use ore_api::instruction::*; use steel::*; -use update::*; pub fn process_instruction( program_id: &Pubkey, @@ -26,14 +21,11 @@ pub fn process_instruction( let (ix, data) = parse_instruction(&ore_api::ID, program_id, data)?; match ix { - OreInstruction::Claim => process_claim(accounts, data)?, + OreInstruction::Bet => process_bet(accounts, data)?, OreInstruction::Close => process_close(accounts, data)?, - OreInstruction::Mine => process_mine(accounts, data)?, - OreInstruction::Open => process_open(accounts, data)?, OreInstruction::Reset => process_reset(accounts, data)?, - OreInstruction::Update => process_update(accounts, data)?, OreInstruction::Initialize => process_initialize(accounts, data)?, - OreInstruction::Migrate => process_migrate(accounts, data)?, + OreInstruction::Payout => process_payout(accounts, data)?, } Ok(()) diff --git a/program/src/migrate.rs b/program/src/migrate.rs deleted file mode 100644 index c14cc5a..0000000 --- a/program/src/migrate.rs +++ /dev/null @@ -1,124 +0,0 @@ -use ore_api::prelude::*; -use solana_program::hash; -use steel::*; - -/// Mine validates hashes and increments a miner's claimable balance. -pub fn process_migrate(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { - // Parse args. - let args = Migrate::try_from_bytes(data)?; - - // Load accounts. - let clock = Clock::get()?; - let t: i64 = clock.unix_timestamp; - let [signer_info, bus_0_info, bus_1_info, bus_2_info, bus_3_info, bus_4_info, bus_5_info, bus_6_info, bus_7_info, config_info, mint_info, treasury_info, treasury_tokens_info, token_program, system_program, slot_hashes_sysvar] = - accounts - else { - return Err(ProgramError::NotEnoughAccountKeys); - }; - signer_info.is_signer()?.has_address(&INITIALIZER_ADDRESS); - let config = config_info - .as_account_mut::(&ore_api::ID)? - .assert_mut_err( - |c| t < c.last_reset_at + EPOCH_DURATION, - OreError::NeedsReset.into(), - )?; - let bus_0 = bus_0_info - .as_account_mut::(&ore_api::ID)? - .assert_mut(|b| b.id == 0)?; - let bus_1 = bus_1_info - .as_account_mut::(&ore_api::ID)? - .assert_mut(|b| b.id == 1)?; - let bus_2 = bus_2_info - .as_account_mut::(&ore_api::ID)? - .assert_mut(|b| b.id == 2)?; - let bus_3 = bus_3_info - .as_account_mut::(&ore_api::ID)? - .assert_mut(|b| b.id == 3)?; - let bus_4 = bus_4_info - .as_account_mut::(&ore_api::ID)? - .assert_mut(|b| b.id == 4)?; - let bus_5 = bus_5_info - .as_account_mut::(&ore_api::ID)? - .assert_mut(|b| b.id == 5)?; - let bus_6 = bus_6_info - .as_account_mut::(&ore_api::ID)? - .assert_mut(|b| b.id == 6)?; - let bus_7 = bus_7_info - .as_account_mut::(&ore_api::ID)? - .assert_mut(|b| b.id == 7)?; - mint_info - .is_writable()? - .has_address(&MINT_ADDRESS)? - .as_mint()?; - treasury_info - .has_address(&TREASURY_ADDRESS)? - .is_writable()?; - treasury_tokens_info - .has_address(&TREASURY_TOKENS_ADDRESS)? - .as_associated_token_account(&TREASURY_ADDRESS, &MINT_ADDRESS)?; - token_program.is_program(&spl_token::ID)?; - system_program.is_program(&system_program::ID)?; - slot_hashes_sysvar.is_sysvar(&sysvar::slot_hashes::ID)?; - - let mut total_bus_balance = 0; - total_bus_balance += bus_0.rewards; - total_bus_balance += bus_1.rewards; - total_bus_balance += bus_2.rewards; - total_bus_balance += bus_3.rewards; - total_bus_balance += bus_4.rewards; - total_bus_balance += bus_5.rewards; - total_bus_balance += bus_6.rewards; - total_bus_balance += bus_7.rewards; - - // Reset bus balances - bus_0.rewards = 0; - bus_1.rewards = 0; - bus_2.rewards = 0; - bus_3.rewards = 0; - bus_4.rewards = 0; - bus_5.rewards = 0; - bus_6.rewards = 0; - bus_7.rewards = 0; - - // Delete bus accounts - bus_0_info.close(signer_info)?; - bus_1_info.close(signer_info)?; - bus_2_info.close(signer_info)?; - bus_3_info.close(signer_info)?; - bus_4_info.close(signer_info)?; - bus_5_info.close(signer_info)?; - bus_6_info.close(signer_info)?; - bus_7_info.close(signer_info)?; - - // Burn all tokens in the bus balances - burn_signed( - treasury_tokens_info, - mint_info, - treasury_info, - token_program, - total_bus_balance, - &[TREASURY], - )?; - - // let proof = proof_info - // .as_account_mut::(&ore_api::ID)? - // .assert_mut_err( - // |p| p.miner == *signer_info.key, - // ProgramError::MissingRequiredSignature, - // )?; - - // Compute the hash. - // let solution = hash::hashv(&[ - // args.nonce.as_slice(), - // config.challenge.as_slice(), - // proof.authority.to_bytes().as_slice(), - // ]); - - // // Update the best solution. - // if solution.to_bytes() < config.best_hash { - // config.best_hash = solution.to_bytes(); - // config.best_proof = *proof_info.key; - // } - - Ok(()) -} diff --git a/program/src/mine.rs b/program/src/mine.rs deleted file mode 100644 index 465680e..0000000 --- a/program/src/mine.rs +++ /dev/null @@ -1,44 +0,0 @@ -use ore_api::prelude::*; -use solana_program::hash; -use steel::*; - -/// Mine validates hashes and increments a miner's claimable balance. -pub fn process_mine(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { - // Parse args. - let args = Mine::try_from_bytes(data)?; - - // Load accounts. - let clock = Clock::get()?; - let t: i64 = clock.unix_timestamp; - let [signer_info, config_info, proof_info] = accounts else { - return Err(ProgramError::NotEnoughAccountKeys); - }; - signer_info.is_signer()?; - let config = config_info.as_account_mut::(&ore_api::ID)?; - let proof = proof_info - .as_account_mut::(&ore_api::ID)? - .assert_mut_err( - |p| p.miner == *signer_info.key, - ProgramError::MissingRequiredSignature, - )?; - - // Compute the hash. - let solution = hash::hashv(&[ - args.nonce.as_slice(), - config.challenge.as_slice(), - proof.authority.to_bytes().as_slice(), - ]); - - // Update the best solution. - if solution.to_bytes() < config.best_hash { - config.best_hash = solution.to_bytes(); - config.best_proof = *proof_info.key; - } - - // Update the proof. - proof.last_hash = solution.to_bytes(); - proof.last_hash_at = t; - proof.total_hashes += 1; - - Ok(()) -} diff --git a/program/src/open.rs b/program/src/open.rs deleted file mode 100644 index 8cd632f..0000000 --- a/program/src/open.rs +++ /dev/null @@ -1,48 +0,0 @@ -use std::mem::size_of; - -use ore_api::prelude::*; -use solana_program::{keccak::hashv, slot_hashes::SlotHash}; -use steel::*; - -/// Open creates a new proof account to track a miner's state. -pub fn process_open(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { - // Load accounts. - let [signer_info, miner_info, payer_info, proof_info, system_program, slot_hashes_info] = - accounts - else { - return Err(ProgramError::NotEnoughAccountKeys); - }; - signer_info.is_signer()?; - payer_info.is_signer()?; - proof_info - .is_empty()? - .is_writable()? - .has_seeds(&[PROOF, signer_info.key.as_ref()], &ore_api::ID)?; - system_program.is_program(&system_program::ID)?; - slot_hashes_info.is_sysvar(&sysvar::slot_hashes::ID)?; - - // Initialize proof. - create_program_account::( - proof_info, - system_program, - payer_info, - &ore_api::ID, - &[PROOF, signer_info.key.as_ref()], - )?; - let clock = Clock::get()?; - let proof = proof_info.as_account_mut::(&ore_api::ID)?; - proof.authority = *signer_info.key; - proof.balance = 0; - proof.challenge = hashv(&[ - signer_info.key.as_ref(), - &slot_hashes_info.data.borrow()[0..size_of::()], - ]) - .0; - proof.last_hash = [0; 32]; - proof.last_hash_at = clock.unix_timestamp; - proof.miner = *miner_info.key; - proof.total_hashes = 0; - proof.total_rewards = 0; - - Ok(()) -} diff --git a/program/src/payout.rs b/program/src/payout.rs new file mode 100644 index 0000000..7d1ea4c --- /dev/null +++ b/program/src/payout.rs @@ -0,0 +1,62 @@ +use std::mem::size_of; + +use ore_api::prelude::*; +use solana_program::{keccak::hashv, slot_hashes::SlotHash}; +use steel::*; + +pub fn process_payout(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { + // Load accounts. + let clock = Clock::get()?; + let [signer_info, block_info, wager_info, recipient_info, treasury_info, treasury_tokens_info, system_program, token_program, slot_hashes_sysvar] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + 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.payed_out == 0)?; + let wager = wager_info.as_account::(&ore_api::ID)?; + recipient_info.as_associated_token_account(&wager.authority, &MINT_ADDRESS)?; + treasury_info.has_address(&TREASURY_ADDRESS)?; + treasury_tokens_info + .has_address(&TREASURY_TOKENS_ADDRESS)? + .is_writable()?; + system_program.is_program(&system_program::ID)?; + token_program.is_program(&spl_token::ID)?; + slot_hashes_sysvar.is_sysvar(&sysvar::slot_hashes::ID)?; + + // Select the slothash from the slot at when the round ended. + // The represents the server seed for the provably fair random number. + let offset = clock.slot - block.ends_at; + let size = size_of::(); + let i = offset as usize * size; + let slot_hash = &slot_hashes_sysvar.data.borrow()[i..i + size]; + block.noise = hashv(&[&block.noise, slot_hash]).to_bytes(); + + // Calculate the random number. + let x = u64::from_le_bytes(block.noise[0..8].try_into().unwrap()); + let y = u64::from_le_bytes(block.noise[8..16].try_into().unwrap()); + let z = u64::from_le_bytes(block.noise[16..24].try_into().unwrap()); + let w = u64::from_le_bytes(block.noise[24..32].try_into().unwrap()); + let roll = (x ^ y ^ z ^ w) % block.total_bets; + + // Assert that the wager account passed in is the winner. + assert!(roll >= wager.cumulative_bets && roll < wager.cumulative_bets + wager.amount); + + // Mark the block as paid out. + block.payed_out = 1; + + // Transfer the winnings to the recipient. + transfer_signed( + &treasury_info, + &treasury_tokens_info, + &recipient_info, + &token_program, + ONE_ORE / 2, + &[TREASURY], + )?; + + Ok(()) +} diff --git a/program/src/reset.rs b/program/src/reset.rs index 9428f56..b5a67e0 100644 --- a/program/src/reset.rs +++ b/program/src/reset.rs @@ -1,102 +1,72 @@ -use drillx::difficulty; use ore_api::prelude::*; -use ore_boost_api::state::Config as BoostConfig; -use solana_program::{hash::hashv, slot_hashes::SlotHash}; +use ore_boost_api::{consts::DENOMINATOR_BPS, prelude::Config as BoostConfig}; use steel::*; /// Reset tops up the bus balances and updates the emissions and reward rates. pub fn process_reset(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { // Load accounts. let clock = Clock::get()?; - let (required_accounts, boost_accounts) = accounts.split_at(7); - let [signer_info, config_info, mint_info, proof_info, treasury_info, treasury_tokens_info, token_program, slot_hashes_sysvar] = + let (required_accounts, boost_accounts) = accounts.split_at(6); + let [signer_info, block_info, mint_info, treasury_info, treasury_tokens_info, token_program, slot_hashes_sysvar] = required_accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; signer_info.is_signer()?; - let config = config_info - .is_config()? - .as_account_mut::(&ore_api::ID)?; + let block = block_info + .as_account_mut::(&ore_api::ID)? + .assert_mut(|b| b.ends_at < clock.slot)? + .assert_mut(|b| b.payed_out != 0)?; let mint = mint_info .has_address(&MINT_ADDRESS)? .is_writable()? .as_mint()?; - let proof = proof_info - .as_account_mut::(&ore_api::ID)? - .assert_mut(|p| p.authority == config.best_proof)?; - treasury_info.is_treasury()?.is_writable()?; - treasury_tokens_info.is_treasury_tokens()?.is_writable()?; + treasury_info.has_address(&TREASURY_ADDRESS)?; + treasury_tokens_info.has_address(&TREASURY_TOKENS_ADDRESS)?; token_program.is_program(&spl_token::ID)?; slot_hashes_sysvar.is_sysvar(&sysvar::slot_hashes::ID)?; - // Parse boost accounts. let [boost_config_info, boost_proof_info] = boost_accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; - let boost_config = boost_config_info.as_account::(&ore_api::ID)?; + let boost_config = boost_config_info.as_account::(&ore_boost_api::ID)?; let boost_proof = boost_proof_info .as_account_mut::(&ore_api::ID)? .assert_mut(|p| p.authority == *boost_config_info.key)?; - // Validate enough time has passed since the last reset. - if clock.unix_timestamp < config.last_reset_at + EPOCH_DURATION { - return Ok(()); - } - - // Record difficulty. - let score = difficulty(config.best_hash) as u64; - - // Reset the challenge. - config.challenge = hashv(&[ - config.challenge.as_slice(), - &slot_hashes_sysvar.data.borrow()[0..size_of::()], - ]) - .to_bytes(); - - // Reset the config. - let block_reward = get_block_reward(mint.supply()); - config.block_reward = block_reward; - config.best_proof = Pubkey::default(); - config.best_hash = [u8::MAX; 32]; - config.last_reset_at = clock.unix_timestamp; - - // Calculate boost reward. - let take_rate = boost_config.take_rate.min(9900); // Cap at 99% - let boost_reward = block_reward * take_rate / ore_boost_api::consts::DENOMINATOR_BPS; - let miner_reward = block_reward - boost_reward; - - // Update proof balances. - proof.balance += miner_reward; - proof.total_rewards += miner_reward; + // Payout to boosts. + let net_emissions = get_target_emissions_rate(mint.supply()); + let boost_reward = + (net_emissions as u128 * boost_config.take_rate as u128 / DENOMINATOR_BPS as u128) as u64; boost_proof.balance += boost_reward; boost_proof.total_rewards += boost_reward; + // Reset the block. + block.reward = net_emissions - boost_reward; + block.started_at = clock.slot; + block.ends_at = clock.slot + 150; // 60 seconds + block.payed_out = 0; + block.total_bets = 0; + block.bet_count = 0; + block.noise = [0; 32]; + block.current_round += 1; + // Fund the treasury. mint_to_signed( mint_info, treasury_tokens_info, treasury_info, token_program, - block_reward, + net_emissions, &[TREASURY], )?; - // Emit event. - BlockEvent { - score, - block_reward, - boost_reward, - ts: clock.unix_timestamp as u64, - } - .log_return(); - Ok(()) } -/// This function calculates the block reward (ORE / min) based on the current supply. -/// It is designed to reduce emissions by 10% approximately every 12 months with a hard stop at 5 million ORE. -pub(crate) fn get_block_reward(current_supply: u64) -> u64 { +/// This function calculates the target emissions rate (ORE / min) based on the current supply. +/// It is designed to reduce emissions by 10% approximately every 12 months with a hardcap at 5 million ORE. +pub(crate) fn get_target_emissions_rate(current_supply: u64) -> u64 { match current_supply { n if n < ONE_ORE * 525_600 => 100_000_000_000, // Year ~1 n if n < ONE_ORE * 998_640 => 90_000_000_000, // Year ~2 @@ -126,52 +96,7 @@ pub(crate) fn get_block_reward(current_supply: u64) -> u64 { n if n < ONE_ORE * 4_916_405 => 7_178_979_874, // Year ~26 n if n < ONE_ORE * 4_950_365 => 6_461_081_886, // Year ~27 n if n < ONE_ORE * 4_980_928 => 5_814_973_607, // Year ~28 - n if n < MAX_SUPPLY => 5_233_476_327.min(MAX_SUPPLY - current_supply), // Year ~29 + n if n < ONE_ORE * 5_000_000 => 5_233_476_327, // Year ~29 _ => 0, } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_block_reward_max_supply() { - let max_supply = ONE_ORE * 5_000_000; - - // Test at max supply - assert_eq!(get_block_reward(max_supply), 0); - - // Test slightly below max supply - let near_max = max_supply - 1; - assert_eq!(get_block_reward(near_max), 1); - - // Test at max supply - 1000 - let below_max = max_supply - 1000; - assert_eq!(get_block_reward(below_max), 1000); - - // Test that reward never exceeds remaining supply - let supply_4_999_990 = ONE_ORE * 4_999_990; - assert!(get_block_reward(supply_4_999_990) <= max_supply - supply_4_999_990); - } - - #[test] - fn test_block_reward_boundaries() { - // Test first tier boundary - let year1_supply = ONE_ORE * 525_599; - assert_eq!(get_block_reward(year1_supply), 100_000_000_000); - - // Test middle tier boundary - let year15_supply = ONE_ORE * 4_173_835; - assert_eq!(get_block_reward(year15_supply), 22_876_792_454); - - // Test last tier boundary before max supply logic - let last_tier_supply = ONE_ORE * 4_980_927; - assert_eq!(get_block_reward(last_tier_supply), 5_814_973_607); - } - - #[test] - fn test_block_reward_zero_supply() { - assert_eq!(get_block_reward(0), 100_000_000_000); - } -} diff --git a/program/src/update.rs b/program/src/update.rs deleted file mode 100644 index 9001ff7..0000000 --- a/program/src/update.rs +++ /dev/null @@ -1,22 +0,0 @@ -use ore_api::prelude::*; -use steel::*; - -/// Update changes the miner authority on a proof account. -pub fn process_update(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { - // Load accounts. - let [signer_info, miner_info, proof_info] = accounts else { - return Err(ProgramError::NotEnoughAccountKeys); - }; - signer_info.is_signer()?; - let proof = proof_info - .as_account_mut::(&ore_api::ID)? - .assert_mut_err( - |p| p.authority == *signer_info.key, - ProgramError::MissingRequiredSignature, - )?; - - // Update the proof's miner authority. - proof.miner = *miner_info.key; - - Ok(()) -}