From 7cba1ff1daf352c1b732d96ce08112dc5828fdaa Mon Sep 17 00:00:00 2001 From: Hardhat Chad Date: Tue, 13 Feb 2024 19:32:31 +0000 Subject: [PATCH] epoch --- Cargo.lock | 10 ++ Cargo.toml | 1 + src/loaders.rs | 100 ++++++++++++++++- src/processor/epoch.rs | 98 ++++++++++++++++- tests/test_epoch.rs | 240 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 444 insertions(+), 5 deletions(-) create mode 100644 tests/test_epoch.rs diff --git a/Cargo.lock b/Cargo.lock index 3d781a8..1ce5857 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -617,6 +617,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" +[[package]] +name = "bs64" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8bebebdf2eda473523fed644eaedc4ffc43b6f35fdc6f001eb9f89dca4bd117" +dependencies = [ + "thiserror", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -2337,6 +2346,7 @@ dependencies = [ name = "ore" version = "0.1.0" dependencies = [ + "bs64", "bytemuck", "num_enum 0.7.2", "shank", diff --git a/Cargo.toml b/Cargo.toml index 145f901..f526aac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ spl-associated-token-account = { version = "^2.2", features = [ "no-entrypoint" static_assertions = "1.1.0" [dev-dependencies] +bs64 = "0.1.2" solana-program-test = "^1.16" solana-sdk = "^1.16" tokio = { version = "1.35", features = ["full"] } diff --git a/src/loaders.rs b/src/loaders.rs index 9ed1d87..c5b2b7f 100644 --- a/src/loaders.rs +++ b/src/loaders.rs @@ -1,5 +1,12 @@ use solana_program::{ - account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, system_program, + account_info::AccountInfo, program_error::ProgramError, program_pack::Pack, pubkey::Pubkey, + system_program, +}; +use spl_token::state::Mint; + +use crate::{ + state::{Bus, Treasury}, + BUS, MINT_ADDRESS, TREASURY, }; pub fn load_signer<'a, 'info>( @@ -42,6 +49,97 @@ pub fn load_uninitialized_pda<'a, 'info>( load_uninitialized_account(info) } +pub fn load_bus<'a, 'info>( + info: &'a AccountInfo<'info>, +) -> Result<&'a AccountInfo<'info>, ProgramError> { + if !info.owner.eq(&crate::id()) { + return Err(ProgramError::InvalidAccountOwner); + } + if info.data_is_empty() { + return Err(ProgramError::UninitializedAccount); + } + + let bus_data = info.data.borrow(); + let bus = bytemuck::try_from_bytes::(&bus_data).unwrap(); + + let key = + Pubkey::create_program_address(&[BUS, &[bus.id as u8], &[bus.bump as u8]], &crate::id())?; + if !info.key.eq(&key) { + return Err(ProgramError::InvalidSeeds); + } + + Ok(info) +} + +pub fn load_treasury<'a, 'info>( + info: &'a AccountInfo<'info>, +) -> Result<&'a AccountInfo<'info>, ProgramError> { + if !info.owner.eq(&crate::id()) { + return Err(ProgramError::InvalidAccountOwner); + } + if info.data_is_empty() { + return Err(ProgramError::UninitializedAccount); + } + + let treasury_data = info.data.borrow(); + let treasury = bytemuck::try_from_bytes::(&treasury_data).unwrap(); + + let key = Pubkey::create_program_address(&[TREASURY, &[treasury.bump as u8]], &crate::id())?; + if !info.key.eq(&key) { + return Err(ProgramError::InvalidSeeds); + } + + Ok(info) +} + +pub fn load_mint<'a, 'info>( + info: &'a AccountInfo<'info>, +) -> Result<&'a AccountInfo<'info>, ProgramError> { + if !info.owner.eq(&spl_token::id()) { + return Err(ProgramError::InvalidAccountOwner); + } + if info.data_is_empty() { + return Err(ProgramError::UninitializedAccount); + } + + let mint_data = info.data.borrow(); + if Mint::unpack_unchecked(&mint_data).is_err() { + return Err(ProgramError::InvalidAccountData); + } + + if !info.key.eq(&MINT_ADDRESS) { + return Err(ProgramError::InvalidAccountData); + } + + Ok(info) +} + +pub fn load_token_account<'a, 'info>( + info: &'a AccountInfo<'info>, + owner: &Pubkey, + mint: &Pubkey, +) -> Result<&'a AccountInfo<'info>, ProgramError> { + if !info.owner.eq(&spl_token::id()) { + return Err(ProgramError::InvalidAccountOwner); + } + if info.data_is_empty() { + return Err(ProgramError::UninitializedAccount); + } + + let account_data = info.data.borrow(); + let account = spl_token::state::Account::unpack_unchecked(&account_data) + .or(Err(ProgramError::InvalidAccountData))?; + + if !account.mint.eq(mint) { + return Err(ProgramError::InvalidAccountData); + } + if !account.owner.eq(owner) { + return Err(ProgramError::InvalidAccountData); + } + + Ok(info) +} + pub fn load_uninitialized_account<'a, 'info>( info: &'a AccountInfo<'info>, ) -> Result<&'a AccountInfo<'info>, ProgramError> { diff --git a/src/processor/epoch.rs b/src/processor/epoch.rs index 5b56bc7..e829be3 100644 --- a/src/processor/epoch.rs +++ b/src/processor/epoch.rs @@ -1,13 +1,103 @@ -use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + clock::Clock, + entrypoint::ProgramResult, + program_error::ProgramError, + pubkey::Pubkey, + sysvar::Sysvar, +}; -use crate::{BUS_EPOCH_REWARDS, SMOOTHING_FACTOR, TARGET_EPOCH_REWARDS}; +use crate::{ + loaders::*, + state::{Bus, Treasury}, + BUS, BUS_COUNT, BUS_EPOCH_REWARDS, EPOCH_DURATION, MAX_EPOCH_REWARDS, SMOOTHING_FACTOR, + TARGET_EPOCH_REWARDS, TREASURY, +}; pub fn process_epoch<'a, 'info>( _program_id: &Pubkey, accounts: &'a [AccountInfo<'info>], - data: &[u8], + _data: &[u8], ) -> ProgramResult { - // TODO + let accounts_iter = &mut accounts.iter(); + + // Account 1: Signer + let _signer = load_signer(next_account_info(accounts_iter)?)?; + + // Accounts 2-9: Busses + let busses = vec![ + load_bus(next_account_info(accounts_iter)?)?, + load_bus(next_account_info(accounts_iter)?)?, + load_bus(next_account_info(accounts_iter)?)?, + load_bus(next_account_info(accounts_iter)?)?, + load_bus(next_account_info(accounts_iter)?)?, + load_bus(next_account_info(accounts_iter)?)?, + load_bus(next_account_info(accounts_iter)?)?, + load_bus(next_account_info(accounts_iter)?)?, + ]; + + // Account 10: Mint + let mint = load_mint(next_account_info(accounts_iter)?)?; + + // Account 11: Treasury + let treasury_info = load_treasury(next_account_info(accounts_iter)?)?; + + // Account 12: Treasury tokens + let treasury_tokens = load_token_account( + next_account_info(accounts_iter)?, + treasury_info.key, + mint.key, + )?; + + // Account 13: Token program + let token_program = load_account(next_account_info(accounts_iter)?, spl_token::id())?; + + // Validate epoch has ended + let clock = Clock::get().unwrap(); + let mut treasury_data = treasury_info.data.borrow_mut(); + let mut treasury = bytemuck::try_from_bytes_mut::(&mut treasury_data).unwrap(); + let epoch_end_at = treasury.epoch_start_at.saturating_add(EPOCH_DURATION); + if !clock.unix_timestamp.ge(&epoch_end_at) { + return Err(ProgramError::Custom(1)); + } + + // Reset busses + let mut total_available_rewards = 0u64; + for i in 0..BUS_COUNT { + let mut bus_data = busses[i].data.borrow_mut(); + let mut bus = bytemuck::try_from_bytes_mut::(&mut bus_data).unwrap(); + total_available_rewards = total_available_rewards.saturating_add(bus.available_rewards); + bus.available_rewards = BUS_EPOCH_REWARDS; + } + + // Update the reward rate for the next epoch + let total_epoch_rewards = MAX_EPOCH_REWARDS.saturating_sub(total_available_rewards); + 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 + let treasury_bump = treasury.bump as u8; + drop(treasury_data); + solana_program::program::invoke_signed( + &spl_token::instruction::mint_to( + &spl_token::id(), + mint.key, + treasury_tokens.key, + treasury_info.key, + &[treasury_info.key], + total_epoch_rewards, + )?, + &[ + token_program.clone(), + mint.clone(), + treasury_tokens.clone(), + treasury_info.clone(), + ], + &[&[TREASURY, &[treasury_bump]]], + )?; + + // TODO Logs? + Ok(()) } diff --git a/tests/test_epoch.rs b/tests/test_epoch.rs new file mode 100644 index 0000000..9a1b0a8 --- /dev/null +++ b/tests/test_epoch.rs @@ -0,0 +1,240 @@ +use std::str::FromStr; + +use ore::{ + instruction::OreInstruction, + state::{Bus, Treasury}, + BUS, BUS_COUNT, BUS_EPOCH_REWARDS, INITIAL_DIFFICULTY, INITIAL_REWARD_RATE, MAX_EPOCH_REWARDS, + MINT, TREASURY, +}; +use solana_program::{ + clock::Clock, + epoch_schedule::DEFAULT_SLOTS_PER_EPOCH, + hash::Hash, + instruction::{AccountMeta, Instruction}, + program_option::COption, + program_pack::Pack, + pubkey::Pubkey, + sysvar, +}; +use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_sdk::{ + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use spl_token::state::{AccountState, Mint}; + +#[tokio::test] +async fn test_epoch() { + // Setup + let (mut banks, payer, hash) = setup_program_test_env().await; + + // Pdas + let bus_pdas = vec![ + Pubkey::find_program_address(&[BUS, &[0]], &ore::id()), + Pubkey::find_program_address(&[BUS, &[1]], &ore::id()), + Pubkey::find_program_address(&[BUS, &[2]], &ore::id()), + Pubkey::find_program_address(&[BUS, &[3]], &ore::id()), + Pubkey::find_program_address(&[BUS, &[4]], &ore::id()), + Pubkey::find_program_address(&[BUS, &[5]], &ore::id()), + Pubkey::find_program_address(&[BUS, &[6]], &ore::id()), + Pubkey::find_program_address(&[BUS, &[7]], &ore::id()), + ]; + let mint_pda = Pubkey::find_program_address(&[MINT], &ore::id()); + let treasury_pda = Pubkey::find_program_address(&[TREASURY], &ore::id()); + let treasury_tokens_address = + spl_associated_token_account::get_associated_token_address(&treasury_pda.0, &mint_pda.0); + + // Build ix + let ix = Instruction { + program_id: ore::ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new(bus_pdas[0].0, false), + AccountMeta::new(bus_pdas[1].0, false), + AccountMeta::new(bus_pdas[2].0, false), + AccountMeta::new(bus_pdas[3].0, false), + AccountMeta::new(bus_pdas[4].0, false), + AccountMeta::new(bus_pdas[5].0, false), + AccountMeta::new(bus_pdas[6].0, false), + AccountMeta::new(bus_pdas[7].0, false), + AccountMeta::new(mint_pda.0, false), + AccountMeta::new(treasury_pda.0, false), + AccountMeta::new(treasury_tokens_address, false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: [OreInstruction::Epoch.to_vec()].concat(), + }; + + // Submit tx + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], hash); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + // Test bus state + for i in 0..BUS_COUNT { + let bus_account = banks.get_account(bus_pdas[i].0).await.unwrap().unwrap(); + assert_eq!(bus_account.owner, ore::id()); + let bus = bytemuck::try_from_bytes::(&bus_account.data).unwrap(); + println!( + "Bus {:?} {:?} {:?}", + bus_pdas[i].0, + bus_account, + bs64::encode(&bus_account.data) + ); + assert_eq!(bus.bump as u8, bus_pdas[i].1); + assert_eq!(bus.id as u8, i as u8); + assert_eq!(bus.available_rewards, BUS_EPOCH_REWARDS); + } + + // Test treasury state + let treasury_account = banks.get_account(treasury_pda.0).await.unwrap().unwrap(); + assert_eq!(treasury_account.owner, ore::id()); + let treasury = bytemuck::try_from_bytes::(&treasury_account.data).unwrap(); + assert_eq!(treasury.bump as u8, treasury_pda.1); + assert_eq!( + treasury.admin, + Pubkey::from_str("EQn4AkZ9UvLcwRgyx1B8Y9sRM3KjfKyti8mjUJW1kL6B").unwrap() + ); + assert_eq!(treasury.difficulty, INITIAL_DIFFICULTY.into()); + assert_eq!(treasury.epoch_start_at as u8, 100); + assert_eq!(treasury.reward_rate, INITIAL_REWARD_RATE.saturating_div(2)); + assert_eq!(treasury.total_claimed_rewards as u8, 0); + println!( + "Treasury {:?} {:?} {:?}", + treasury_pda.0, + treasury_account, + bs64::encode(&treasury_account.data) + ); + + // Test mint state + let mint_account = banks.get_account(mint_pda.0).await.unwrap().unwrap(); + assert_eq!(mint_account.owner, spl_token::id()); + let mint = Mint::unpack(&mint_account.data).unwrap(); + assert_eq!(mint.mint_authority, COption::Some(treasury_pda.0)); + assert_eq!(mint.supply, MAX_EPOCH_REWARDS); + assert_eq!(mint.decimals, ore::TOKEN_DECIMALS); + assert_eq!(mint.is_initialized, true); + assert_eq!(mint.freeze_authority, COption::None); + println!( + "Mint {:?} {:?} {:?}", + mint_pda.0, + mint_account, + bs64::encode(&mint_account.data) + ); + + // Test treasury token state + let treasury_tokens_account = banks + .get_account(treasury_tokens_address) + .await + .unwrap() + .unwrap(); + assert_eq!(treasury_tokens_account.owner, spl_token::id()); + let treasury_tokens = spl_token::state::Account::unpack(&treasury_tokens_account.data).unwrap(); + assert_eq!(treasury_tokens.mint, mint_pda.0); + assert_eq!(treasury_tokens.owner, treasury_pda.0); + assert_eq!(treasury_tokens.amount, MAX_EPOCH_REWARDS); + assert_eq!(treasury_tokens.delegate, COption::None); + assert_eq!(treasury_tokens.state, AccountState::Initialized); + assert_eq!(treasury_tokens.is_native, COption::None); + assert_eq!(treasury_tokens.delegated_amount, 0); + assert_eq!(treasury_tokens.close_authority, COption::None); + println!( + "Treasury tokens {:?} {:?} {:?}", + treasury_tokens_address, + treasury_tokens_account, + bs64::encode(&treasury_tokens_account.data) + ); +} + +async fn setup_program_test_env() -> (BanksClient, Keypair, Hash) { + let mut program_test = ProgramTest::new("ore", ore::ID, processor!(ore::process_instruction)); + program_test.prefer_bpf(true); + + // Busses + program_test.add_account_with_base64_data( + Pubkey::from_str("2uwqyH2gKqstgAFCSniirx73X4iQek5ETc2vVJKUiNMg").unwrap(), + 1002240, + ore::id(), + "/wAAAAAAAAAAAAAAAAAAAA==", + ); + program_test.add_account_with_base64_data( + Pubkey::from_str("FRMC6jVczm1cRaEs5EhDsfw7X8vsmSDpf3bJWVkawngu").unwrap(), + 1002240, + ore::id(), + "/gAAAAEAAAAAAAAAAAAAAA==", + ); + program_test.add_account_with_base64_data( + Pubkey::from_str("9nWyycs4GHjnLujPR2sbA1A8K8CkiLc5VzxWUD4hg2uM").unwrap(), + 1002240, + ore::id(), + "/wAAAAIAAAAAAAAAAAAAAA==", + ); + program_test.add_account_with_base64_data( + Pubkey::from_str("Kt7kqD3MyvxLbj4ek9urXUxkDoxaMuQn82K2VdYD1jM").unwrap(), + 1002240, + ore::id(), + "+gAAAAMAAAAAAAAAAAAAAA==", + ); + program_test.add_account_with_base64_data( + Pubkey::from_str("8r9mXYnFQXhwrNfvatGUTxbbNSqxScuCwp4sBTSxDVTJ").unwrap(), + 1002240, + ore::id(), + "/QAAAAQAAAAAAAAAAAAAAA==", + ); + program_test.add_account_with_base64_data( + Pubkey::from_str("D9cEH32k8p9uWc4w5RrStK9rWssU8NuX1Dg5YaUim4wL").unwrap(), + 1002240, + ore::id(), + "/wAAAAUAAAAAAAAAAAAAAA==", + ); + program_test.add_account_with_base64_data( + Pubkey::from_str("H1RKMYADPzd4C1j1RZu51NvRSVktoTYEJyeVy98Kmdyu").unwrap(), + 1002240, + ore::id(), + "/wAAAAYAAAAAAAAAAAAAAA==", + ); + program_test.add_account_with_base64_data( + Pubkey::from_str("3XbdZNbBjjp8qnDJjv1RxaKisyfx6ahznYkSigs6dayy").unwrap(), + 1002240, + ore::id(), + "+QAAAAcAAAAAAAAAAAAAAA==", + ); + + // Treasury + program_test.add_account_with_base64_data( + Pubkey::from_str("67PLJej6iZm915WbEu6NLeZtRZtnHc5nSVQvkHRZyPiC").unwrap(), + 1559040, + ore::id(), + "/wAAAAAAAADHPztpT4Jpqy1n9x6y1psKOUdDt07/OgR6noRFAOuOcAAA////////////////////////////////////////AAAAAAAAAADoAwAAAAAAAAAAAAAAAAAA", + ); + + // Mint + program_test.add_account_with_base64_data( + Pubkey::from_str("DY4JVebraRXg9BGt4MRU4mvqHGDzmi2Ay1HGjDU5YeNf").unwrap(), + 1461600, + spl_token::id(), + "AQAAAEvtK9pjA/sPMEl3rhUgX8iz4/q0A5icrVGp0GdL3satAAAAAAAAAAAJAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + ); + + // Treasury tokens + program_test.add_account_with_base64_data( + Pubkey::from_str("EH4tskvkeNqX5ce3FBr4oJob3FKSns9th7NvP28ZHsNL").unwrap(), + 2039280, + spl_token::id(), + "ukD7Oc0QjzbigRIB1x9/XLzAT3w7X0UTZ1NVeB85lRRL7SvaYwP7DzBJd64VIF/Is+P6tAOYnK1RqdBnS97GrQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + ); + + // Set sysvar + program_test.add_sysvar_account( + sysvar::clock::id(), + &Clock { + slot: 10, + epoch_start_timestamp: 0, + epoch: 0, + leader_schedule_epoch: DEFAULT_SLOTS_PER_EPOCH, + unix_timestamp: 100, + }, + ); + + program_test.start().await +}