diff --git a/Cargo.lock b/Cargo.lock index 07e543e..8ed1112 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1325,6 +1325,7 @@ dependencies = [ "solana-program", "spl-associated-token-account", "spl-token 4.0.2", + "spl-token-2022 7.0.0", "steel", ] diff --git a/Cargo.toml b/Cargo.toml index 6a82026..5f394c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ solana-program = "^2.1" solana-client = "^2.1" solana-sdk = "^2.1" spl-token = { version = "^4", features = ["no-entrypoint"] } +spl-token-2022 = "^7" spl-associated-token-account = { version = "^6", features = [ "no-entrypoint" ] } steel = { features = ["spl"], version = "4.0" } thiserror = "1.0.57" diff --git a/README.md b/README.md index 6f67b88..f94cda5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ORE -**Digital gold, accelerated.** +**Mine blockspace. Trade hashpower. Win rewards.** ## API - [`Consts`](api/src/consts.rs) – Program constants. @@ -9,18 +9,28 @@ - [`Instruction`](api/src/instruction.rs) – Declared instructions and arguments. ## Instructions -- [`Bury`](program/src/bury.rs) - Swap committed tokens into ORE and burns it. -- [`Close`](program/src/close.rs) - Close a commit account. -- [`Deploy`](program/src/deploy.rs) - Deploy capital to mine the current block. -- [`Initialize`](program/src/initialize.rs) - Initialize the program. -- [`Payout`](program/src/payout.rs) - Payout the block reward to the winning commit. -- [`Reset`](program/src/reset.rs) - Start the next block. + +#### Mine +- [`Open`](program/src/open.rs) - Opens a new block for mining. +- [`Close`](program/src/close.rs) - Closes a block and pays out rewards. +- [`Mine`](program/src/mine.rs) - Mines the current block by computing hashes. + +#### Stake +- [`Deposit`](program/src/deposit.rs) - Deposits stake into a miner account. +- [`Withdraw`](program/src/withdraw.rs) - Withdraws stake from a miner account. +- [`Free`](program/src/free.rs) - Frees up miner capacity after block ends. + +#### Trade +- [`Buy`](program/src/buy.rs) - Buys hash tokens from the market. +- [`Sell`](program/src/sell.rs) - Sells hash tokens to the market. ## State -- [`Block`](api/src/state/block.rs) - A singleton account tracking rounds of commits. -- [`Proof`](api/src/state/proof.rs) - (Deprecated) An account which tracks a miner's current hash and current stake. -- [`Treasury`](api/src/state/treasury.rs) – The mint authority on the ORE token. -- [`Commit`](api/src/state/commit.rs) - Capital deployed by a miner in the current block. +- [`Block`](api/src/state/block.rs) - A period of time for mining. +- [`Config`](api/src/state/config.rs) - Global program configuration. +- [`Market`](api/src/state/market.rs) - Hashpower market for a given block. +- [`Miner`](api/src/state/miner.rs) - A user's mining and staking state. +- [`Receipt`](api/src/state/receipt.rs) - Tracks a miner's deployed capital. +- [`Treasury`](api/src/state/treasury.rs) - The mint authority on the ORE token. ## Tests diff --git a/api/src/instruction.rs b/api/src/instruction.rs index 348ea96..7882b8a 100644 --- a/api/src/instruction.rs +++ b/api/src/instruction.rs @@ -3,19 +3,19 @@ use steel::*; #[repr(u8)] #[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] pub enum OreInstruction { - // Block + // Mine Open = 0, Close = 1, Mine = 2, - // Market - Buy = 3, - Sell = 4, - // Stake - Deposit = 5, - Withdraw = 6, - Free = 7, + Deposit = 3, + Withdraw = 4, + Free = 5, + + // Trade + Buy = 6, + Sell = 7, } #[repr(C)] @@ -34,6 +34,22 @@ pub struct Mine { pub amount: [u8; 8], } +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct Deposit { + pub amount: [u8; 8], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct Withdraw { + pub amount: [u8; 8], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct Free {} + #[repr(C)] #[derive(Clone, Copy, Debug, Pod, Zeroable)] pub struct Buy {} @@ -42,23 +58,11 @@ pub struct Buy {} #[derive(Clone, Copy, Debug, Pod, Zeroable)] pub struct Sell {} -#[repr(C)] -#[derive(Clone, Copy, Debug, Pod, Zeroable)] -pub struct Deposit {} - -#[repr(C)] -#[derive(Clone, Copy, Debug, Pod, Zeroable)] -pub struct Withdraw {} - -#[repr(C)] -#[derive(Clone, Copy, Debug, Pod, Zeroable)] -pub struct Free {} - instruction!(OreInstruction, Open); instruction!(OreInstruction, Close); instruction!(OreInstruction, Mine); -instruction!(OreInstruction, Buy); -instruction!(OreInstruction, Sell); instruction!(OreInstruction, Deposit); instruction!(OreInstruction, Withdraw); instruction!(OreInstruction, Free); +instruction!(OreInstruction, Buy); +instruction!(OreInstruction, Sell); diff --git a/api/src/state/market.rs b/api/src/state/market.rs index 4c92d5d..bfae8f6 100644 --- a/api/src/state/market.rs +++ b/api/src/state/market.rs @@ -6,7 +6,10 @@ use super::OreAccount; #[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] pub struct Market { /// The id of the block this market is associated with. - pub block_id: u64, + pub id: u64, + + /// Mint of the hash token. + pub mint: Pubkey, } // TODO Bonding curve stuff diff --git a/api/src/state/miner.rs b/api/src/state/miner.rs index e62ad94..4bfec1a 100644 --- a/api/src/state/miner.rs +++ b/api/src/state/miner.rs @@ -11,8 +11,8 @@ pub struct Miner { /// The ID of the last block this miner mined in. pub block_id: u64, - /// The amount of ORE this miner can deploy into hashpower markets. - pub capacity: u64, + /// The amount of ORE this miner has deployed into hashpower markets. + pub deployed: u64, /// The hash of the last block this miner mined in. pub hash: [u8; 32], diff --git a/api/src/state/treasury.rs b/api/src/state/treasury.rs index 2268ae0..c8ccaf4 100644 --- a/api/src/state/treasury.rs +++ b/api/src/state/treasury.rs @@ -6,6 +6,9 @@ use super::OreAccount; /// the program's global token account. #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -pub struct Treasury {} +pub struct Treasury { + /// The total amount of ORE that has been staked. + pub total_stake: u64, +} account!(OreAccount, Treasury); diff --git a/program/Cargo.toml b/program/Cargo.toml index f180729..0cf106f 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -25,6 +25,7 @@ ore-api.workspace = true ore-boost-api.workspace = true solana-program.workspace = true spl-token.workspace = true +spl-token-2022.workspace = true spl-associated-token-account.workspace = true steel.workspace = true diff --git a/program/src/buy.rs b/program/src/buy.rs index e69de29..c41410a 100644 --- a/program/src/buy.rs +++ b/program/src/buy.rs @@ -0,0 +1,23 @@ +use ore_api::prelude::*; +use steel::*; + +/// Buy hashpower. +pub fn process_buy(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { + // Load accounts. + let clock = Clock::get()?; + let [signer_info, block_info, market_info, miner_info, recipient_info, system_program, token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + signer_info.is_signer()?; + let block = block_info + .as_account_mut::(&ore_api::ID)? + .assert_mut(|b| clock.slot >= b.start_slot + 1500)?; + system_program.is_program(&system_program::ID)?; + token_program.is_program(&spl_token::ID)?; + + // TODO Buy hash tokens + + Ok(()) +} diff --git a/program/src/close.rs b/program/src/close.rs index e69de29..22e140b 100644 --- a/program/src/close.rs +++ b/program/src/close.rs @@ -0,0 +1,69 @@ +use ore_api::prelude::*; +use steel::*; + +/// Closes a block. +pub fn process_close(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { + // Load accounts. + let clock = Clock::get()?; + let [signer_info, block_info, market_info, market_hash_info, market_ore_info, mint_hash_info, mint_ore_info, recipient_info, treasury_info, system_program, token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + signer_info.is_signer()?; + let block = block_info + .as_account_mut::(&ore_api::ID)? + .assert_mut(|b| clock.slot >= b.start_slot + 1500)?; + let market = market_info + .as_account_mut::(&ore_api::ID)? + .assert_mut(|m| m.id == block.id)?; + let market_hash = + market_hash_info.as_associated_token_account(market_info.key, mint_hash_info.key)?; + let market_ore = + market_ore_info.as_associated_token_account(market_info.key, mint_ore_info.key)?; + mint_hash_info.has_address(&market.mint)?; + mint_ore_info.has_address(&MINT_ADDRESS)?; + system_program.is_program(&system_program::ID)?; + token_program.is_program(&spl_token::ID)?; + + // Payout block reward. + if block.best_miner != Pubkey::default() { + recipient_info.as_associated_token_account(&block.best_miner, &MINT_ADDRESS)?; + mint_to_signed( + mint_ore_info, + recipient_info, + treasury_info, + token_program, + block.reward, + &[TREASURY], + )?; + } + + // Burn hash tokens. + burn_signed( + market_hash_info, + mint_hash_info, + market_info, + token_program, + market_hash.amount(), + &[MARKET, &market.id.to_le_bytes()], + )?; + + // Burn ORE liquidity. + burn_signed( + market_ore_info, + mint_ore_info, + market_info, + token_program, + market_ore.amount(), + &[MARKET, &market.id.to_le_bytes()], + )?; + + // Close block. + block_info.close(signer_info)?; + + // Close market. + market_info.close(signer_info)?; + + Ok(()) +} diff --git a/program/src/deposit.rs b/program/src/deposit.rs index e69de29..76cf0fc 100644 --- a/program/src/deposit.rs +++ b/program/src/deposit.rs @@ -0,0 +1,39 @@ +use ore_api::prelude::*; +use steel::*; + +/// Deposits stake. +pub fn process_deposit(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { + // Parse data. + let args = Deposit::try_from_bytes(data)?; + let amount = u64::from_le_bytes(args.amount); + + // Load accounts. + let [signer_info, miner_info, sender_info, treasury_info, treasury_tokens_info, token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + signer_info.is_signer()?; + let miner = miner_info + .as_account_mut::(&ore_api::ID)? + .assert_mut(|m| m.authority == *signer_info.key)?; + sender_info.as_associated_token_account(&signer_info.key, &MINT_ADDRESS)?; + let treasury = treasury_info.as_account_mut::(&ore_api::ID)?; + treasury_tokens_info.as_associated_token_account(&treasury_info.key, &MINT_ADDRESS)?; + token_program.is_program(&spl_token::ID)?; + + // Update account state. + miner.stake += amount; + treasury.total_stake += amount; + + // Execute transfer. + transfer( + signer_info, + sender_info, + treasury_tokens_info, + token_program, + amount, + )?; + + Ok(()) +} diff --git a/program/src/free.rs b/program/src/free.rs index e69de29..f19e026 100644 --- a/program/src/free.rs +++ b/program/src/free.rs @@ -0,0 +1,31 @@ +use ore_api::prelude::*; +use steel::*; + +/// Free up capacity. +pub fn process_free(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { + // Load accounts. + let clock = Clock::get()?; + let [signer_info, miner_info, receipt_info] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + signer_info.is_signer()?; + let miner = miner_info + .as_account_mut::(&ore_api::ID)? + .assert_mut(|m| m.authority == *signer_info.key)?; + let receipt = receipt_info + .as_account_mut::(&ore_api::ID)? + .assert_mut(|r| r.authority == *signer_info.key)?; + + // Asset that block has ended. + let start_slot = 1500 * receipt.block_id; + let end_slot = start_slot + 1500; + assert!(clock.slot >= end_slot, "Block has not yet closed."); + + // Free up miner capacity. + miner.deployed -= receipt.amount; + + // Close the receipt. + receipt_info.close(signer_info)?; + + Ok(()) +} diff --git a/program/src/lib.rs b/program/src/lib.rs index 886bc7d..fe2d02c 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -27,10 +27,19 @@ pub fn process_instruction( let (ix, data) = parse_instruction(&ore_api::ID, program_id, data)?; match ix { + // Mine OreInstruction::Open => process_open(accounts, data)?, + OreInstruction::Close => process_close(accounts, data)?, OreInstruction::Mine => process_mine(accounts, data)?, - _ => panic!("Not implemented"), + // Stake + OreInstruction::Deposit => process_deposit(accounts, data)?, + OreInstruction::Withdraw => process_withdraw(accounts, data)?, + OreInstruction::Free => process_free(accounts, data)?, + + // Trade + OreInstruction::Buy => process_buy(accounts, data)?, + OreInstruction::Sell => process_sell(accounts, data)?, } Ok(()) diff --git a/program/src/mine.rs b/program/src/mine.rs index 1c3ddaa..db5863f 100644 --- a/program/src/mine.rs +++ b/program/src/mine.rs @@ -2,7 +2,7 @@ use ore_api::prelude::*; use solana_program::keccak; use steel::*; -/// Opens a new block for hashpower trading. +/// Mine a block. pub fn process_mine(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { // Parse data. let args = Mine::try_from_bytes(data)?; diff --git a/program/src/open.rs b/program/src/open.rs index 75c70e7..315b8b4 100644 --- a/program/src/open.rs +++ b/program/src/open.rs @@ -1,14 +1,15 @@ use ore_api::prelude::*; +use solana_program::program_pack::Pack; use steel::*; -/// Opens a new block for hashpower trading. +/// Opens a new block. pub fn process_open(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { // Parse data. let args = Open::try_from_bytes(data)?; let id = u64::from_le_bytes(args.id); // Load accounts. - let [signer_info, block_info, system_program, token_program, associated_token_program] = + let [signer_info, block_info, market_info, market_hash_info, market_ore_info, mint_hash_info, mint_ore_info, system_program, token_program, associated_token_program, rent_sysvar] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); @@ -18,9 +19,19 @@ pub fn process_open(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult .is_empty()? .is_writable()? .has_seeds(&[BLOCK, &id.to_le_bytes()], &ore_api::ID)?; + market_info + .is_empty()? + .is_writable()? + .has_seeds(&[MARKET, &id.to_le_bytes()], &ore_api::ID)?; + mint_hash_info + .is_empty()? + .is_writable()? + .has_seeds(&[MINT, &id.to_le_bytes()], &ore_api::ID)?; + mint_ore_info.has_address(&MINT_ADDRESS)?.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)?; + rent_sysvar.is_sysvar(&sysvar::rent::ID)?; // Initialize config. create_program_account::( @@ -38,30 +49,85 @@ pub fn process_open(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult block.slot_hash = [0; 32]; block.start_slot = 1500 * id; - // TODO Init market - // TODO Init hash token mint - // TODO Init token accounts for market - // TODO Init mint hash tokens to market + // Initialize market. + create_program_account::( + market_info, + system_program, + signer_info, + &ore_api::ID, + &[MARKET, &id.to_le_bytes()], + )?; + let market = market_info.as_account_mut::(&ore_api::ID)?; + market.id = id; - // // Initialize block token accounts. - // create_associated_token_account( - // signer_info, - // block_info, - // block_commits_info, - // sol_mint_info, - // system_program, - // token_program, - // associated_token_program, - // )?; - // create_associated_token_account( - // signer_info, - // block_info, - // block_ore_info, - // ore_mint_info, - // system_program, - // token_program, - // associated_token_program, - // )?; + // Initialize hash token mint. + allocate_account( + mint_hash_info, + system_program, + signer_info, + spl_token::state::Mint::LEN, + &spl_token::ID, + &[MINT, &id.to_le_bytes()], + )?; + initialize_mint_signed( + mint_hash_info, + block_info, + None, + token_program, + rent_sysvar, + 0, + &[MINT, &id.to_le_bytes()], + )?; + + // TODO Initialize hash token metadata. + + // Initialize token accounts for market. + if market_hash_info.data_is_empty() { + create_associated_token_account( + signer_info, + market_info, + market_hash_info, + mint_hash_info, + system_program, + token_program, + associated_token_program, + )?; + } else { + market_hash_info.as_associated_token_account(market_info.key, mint_hash_info.key)?; + } + if market_ore_info.data_is_empty() { + create_associated_token_account( + signer_info, + market_info, + market_ore_info, + mint_ore_info, + system_program, + token_program, + associated_token_program, + )?; + } else { + market_ore_info.as_associated_token_account(market_info.key, mint_ore_info.key)?; + } + + // Mint hash tokens to market. + mint_to_signed( + mint_hash_info, + market_hash_info, + block_info, + token_program, + 10_000_000, + &[BLOCK, &id.to_le_bytes()], + )?; + + // Burn mint authority. + set_authority_signed( + mint_hash_info, + block_info, + None, + spl_token_2022::instruction::AuthorityType::MintTokens, + token_program, + &[BLOCK, &id.to_le_bytes()], + )?; Ok(()) } diff --git a/program/src/sell.rs b/program/src/sell.rs index e69de29..ca10b9a 100644 --- a/program/src/sell.rs +++ b/program/src/sell.rs @@ -0,0 +1,23 @@ +use ore_api::prelude::*; +use steel::*; + +/// Sell hashpower. +pub fn process_sell(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { + // Load accounts. + let clock = Clock::get()?; + let [signer_info, block_info, market_info, miner_info, recipient_info, system_program, token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + signer_info.is_signer()?; + let block = block_info + .as_account_mut::(&ore_api::ID)? + .assert_mut(|b| clock.slot >= b.start_slot + 1500)?; + system_program.is_program(&system_program::ID)?; + token_program.is_program(&spl_token::ID)?; + + // TODO Buy hash tokens + + Ok(()) +} diff --git a/program/src/withdraw.rs b/program/src/withdraw.rs index e69de29..d4215f0 100644 --- a/program/src/withdraw.rs +++ b/program/src/withdraw.rs @@ -0,0 +1,46 @@ +use ore_api::prelude::*; +use steel::*; + +/// Withdraws stake. +pub fn process_withdraw(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { + // Parse data. + let args = Withdraw::try_from_bytes(data)?; + let amount = u64::from_le_bytes(args.amount); + + // Load accounts. + let [signer_info, miner_info, recipient_info, treasury_info, treasury_tokens_info, token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + signer_info.is_signer()?; + let miner = miner_info + .as_account_mut::(&ore_api::ID)? + .assert_mut(|m| m.authority == *signer_info.key)?; + recipient_info.as_associated_token_account(&signer_info.key, &MINT_ADDRESS)?; + let treasury = treasury_info.as_account_mut::(&ore_api::ID)?; + treasury_tokens_info.as_associated_token_account(&treasury_info.key, &MINT_ADDRESS)?; + token_program.is_program(&spl_token::ID)?; + + // Update account state. + miner.stake -= amount; + treasury.total_stake -= amount; + + // Asset miner has enough stake to cover the withdrawal. + assert!( + miner.stake >= miner.deployed, + "Withdrawal cannot reduce capacity below deployment levels." + ); + + // Execute transfer. + transfer_signed( + signer_info, + treasury_tokens_info, + recipient_info, + token_program, + amount, + &[TREASURY], + )?; + + Ok(()) +}