From cb308174470a6431e41d8b5cda26d363faa5aeca Mon Sep 17 00:00:00 2001 From: Hardhat Chad Date: Tue, 13 Feb 2024 07:17:12 +0000 Subject: [PATCH] Begin native rewrite without anchor --- Anchor.toml | 18 - Cargo.lock | 339 ++++------------- Cargo.toml | 44 ++- programs/ore/Cargo.toml | 32 -- programs/ore/Xargo.toml | 2 - programs/ore/src/lib.rs | 795 --------------------------------------- rust-toolchain.toml | 3 +- src/instruction.rs | 103 +++++ src/lib.rs | 514 +++++++++++++++++++++++++ src/loaders.rs | 65 ++++ tests/test_initialize.rs | 121 ++++++ 11 files changed, 910 insertions(+), 1126 deletions(-) delete mode 100644 Anchor.toml delete mode 100644 programs/ore/Cargo.toml delete mode 100644 programs/ore/Xargo.toml delete mode 100644 programs/ore/src/lib.rs create mode 100644 src/instruction.rs create mode 100644 src/lib.rs create mode 100644 src/loaders.rs create mode 100644 tests/test_initialize.rs diff --git a/Anchor.toml b/Anchor.toml deleted file mode 100644 index 5885a1c..0000000 --- a/Anchor.toml +++ /dev/null @@ -1,18 +0,0 @@ -[toolchain] - -[features] -seeds = false -skip-lint = false - -[programs.localnet] -ore = "CeJShZEAzBLwtcLQvbZc7UT38e4nUTn63Za5UFyYYDTS" - -[registry] -url = "https://api.apr.dev" - -[provider] -cluster = "Localnet" -wallet = "/home/ubuntu/.config/solana/id.json" - -[scripts] -test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" diff --git a/Cargo.lock b/Cargo.lock index 4c20453..3d781a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,167 +117,6 @@ dependencies = [ "alloc-no-stdlib", ] -[[package]] -name = "anchor-attribute-access-control" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5f619f1d04f53621925ba8a2e633ba5a6081f2ae14758cbb67f38fd823e0a3e" -dependencies = [ - "anchor-syn", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "anchor-attribute-account" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f2a3e1df4685f18d12a943a9f2a7456305401af21a07c9fe076ef9ecd6e400" -dependencies = [ - "anchor-syn", - "bs58 0.5.0", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "anchor-attribute-constant" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9423945cb55627f0b30903288e78baf6f62c6c8ab28fb344b6b25f1ffee3dca7" -dependencies = [ - "anchor-syn", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "anchor-attribute-error" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ed12720033cc3c3bf3cfa293349c2275cd5ab99936e33dd4bf283aaad3e241" -dependencies = [ - "anchor-syn", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "anchor-attribute-event" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eef4dc0371eba2d8c8b54794b0b0eb786a234a559b77593d6f80825b6d2c77a2" -dependencies = [ - "anchor-syn", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "anchor-attribute-program" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b18c4f191331e078d4a6a080954d1576241c29c56638783322a18d308ab27e4f" -dependencies = [ - "anchor-syn", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "anchor-derive-accounts" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de10d6e9620d3bcea56c56151cad83c5992f50d5960b3a9bebc4a50390ddc3c" -dependencies = [ - "anchor-syn", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "anchor-derive-serde" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4e2e5be518ec6053d90a2a7f26843dbee607583c779e6c8395951b9739bdfbe" -dependencies = [ - "anchor-syn", - "borsh-derive-internal 0.10.3", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "anchor-derive-space" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ecc31d19fa54840e74b7a979d44bcea49d70459de846088a1d71e87ba53c419" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "anchor-lang" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35da4785497388af0553586d55ebdc08054a8b1724720ef2749d313494f2b8ad" -dependencies = [ - "anchor-attribute-access-control", - "anchor-attribute-account", - "anchor-attribute-constant", - "anchor-attribute-error", - "anchor-attribute-event", - "anchor-attribute-program", - "anchor-derive-accounts", - "anchor-derive-serde", - "anchor-derive-space", - "arrayref", - "base64 0.13.1", - "bincode", - "borsh 0.10.3", - "bytemuck", - "getrandom 0.2.11", - "solana-program", - "thiserror", -] - -[[package]] -name = "anchor-spl" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c4fd6e43b2ca6220d2ef1641539e678bfc31b6cc393cf892b373b5997b6a39a" -dependencies = [ - "anchor-lang", - "solana-program", - "spl-associated-token-account", - "spl-token", - "spl-token-2022 0.9.0", -] - -[[package]] -name = "anchor-syn" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9101b84702fed2ea57bd22992f75065da5648017135b844283a2f6d74f27825" -dependencies = [ - "anyhow", - "bs58 0.5.0", - "heck 0.3.3", - "proc-macro2", - "quote", - "serde", - "serde_json", - "sha2 0.10.8", - "syn 1.0.109", - "thiserror", -] - [[package]] name = "android-tzdata" version = "0.1.1" @@ -778,15 +617,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" -[[package]] -name = "bs58" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5353f36341f7451062466f0b755b96ac3a9547e4d7f6b70d603fc721a7d7896" -dependencies = [ - "tinyvec", -] - [[package]] name = "bumpalo" version = "3.14.0" @@ -805,9 +635,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.14.0" +version = "1.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" +checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f" dependencies = [ "bytemuck_derive", ] @@ -1735,15 +1565,6 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "heck" version = "0.4.1" @@ -2420,11 +2241,11 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683751d591e6d81200c39fb0d1032608b77724f34114db54f571ff1317b337c0" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" dependencies = [ - "num_enum_derive 0.7.1", + "num_enum_derive 0.7.2", ] [[package]] @@ -2441,9 +2262,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c11e44798ad209ccdd91fc192f0526a369a01234f7373e1b141c96d7cee4f0e" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ "proc-macro-crate 2.0.1", "proc-macro2", @@ -2516,12 +2337,16 @@ dependencies = [ name = "ore" version = "0.1.0" dependencies = [ - "anchor-lang", - "anchor-spl", - "bincode", + "bytemuck", + "num_enum 0.7.2", + "shank", "solana-program", "solana-program-test", + "solana-sdk", + "spl-associated-token-account", + "spl-token", "static_assertions", + "tokio", ] [[package]] @@ -3402,6 +3227,52 @@ dependencies = [ "keccak", ] +[[package]] +name = "shank" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c9395612d493b69a522725eef78a095f199d43eeb847f4a4b77ec0cacab535" +dependencies = [ + "shank_macro", +] + +[[package]] +name = "shank_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8abef069c02e15f62233679b1e71f3152fac10f90b3ff89ebbad6a25b7497754" +dependencies = [ + "proc-macro2", + "quote", + "shank_macro_impl", + "shank_render", + "syn 1.0.109", +] + +[[package]] +name = "shank_macro_impl" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d3d92bfcc6e08f882f2264d774d1a2f46dc36122adc1b76416ba6405a29a9c" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "serde", + "syn 1.0.109", +] + +[[package]] +name = "shank_render" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a2ea9c6dd95ea311b3b81e63cf4e9c808ed04b098819e6d2c4b1a467d587203" +dependencies = [ + "proc-macro2", + "quote", + "shank_macro_impl", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -3482,7 +3353,7 @@ dependencies = [ "Inflector", "base64 0.21.5", "bincode", - "bs58 0.4.0", + "bs58", "bv", "lazy_static", "serde", @@ -3491,7 +3362,7 @@ dependencies = [ "solana-config-program", "solana-sdk", "spl-token", - "spl-token-2022 1.0.0", + "spl-token-2022", "spl-token-group-interface", "spl-token-metadata-interface", "thiserror", @@ -3792,7 +3663,7 @@ dependencies = [ "ahash 0.8.6", "blake3", "block-buffer 0.10.4", - "bs58 0.4.0", + "bs58", "bv", "byteorder", "cc", @@ -3941,7 +3812,7 @@ dependencies = [ "blake3", "borsh 0.10.3", "borsh 0.9.3", - "bs58 0.4.0", + "bs58", "bv", "bytemuck", "cc", @@ -4127,7 +3998,7 @@ dependencies = [ "async-trait", "base64 0.21.5", "bincode", - "bs58 0.4.0", + "bs58", "indicatif", "log", "reqwest", @@ -4151,7 +4022,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "131662e5eea4fa5fc88b01f07d9e430315c0976be848ba3994244249c5fb033a" dependencies = [ "base64 0.21.5", - "bs58 0.4.0", + "bs58", "jsonrpc-core", "reqwest", "semver", @@ -4162,7 +4033,7 @@ dependencies = [ "solana-sdk", "solana-transaction-status", "solana-version", - "spl-token-2022 1.0.0", + "spl-token-2022", "thiserror", ] @@ -4267,7 +4138,7 @@ dependencies = [ "bincode", "bitflags 2.4.1", "borsh 0.10.3", - "bs58 0.4.0", + "bs58", "bytemuck", "byteorder", "chrono", @@ -4316,7 +4187,7 @@ version = "1.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60f58786e949f43b8c9b826fdfa5ad8586634b077ab04f989fb8e30535786712" dependencies = [ - "bs58 0.4.0", + "bs58", "proc-macro2", "quote", "rustversion", @@ -4455,7 +4326,7 @@ dependencies = [ "base64 0.21.5", "bincode", "borsh 0.10.3", - "bs58 0.4.0", + "bs58", "lazy_static", "log", "serde", @@ -4466,7 +4337,7 @@ dependencies = [ "spl-associated-token-account", "spl-memo", "spl-token", - "spl-token-2022 1.0.0", + "spl-token-2022", "thiserror", ] @@ -4638,7 +4509,7 @@ dependencies = [ "num-traits", "solana-program", "spl-token", - "spl-token-2022 1.0.0", + "spl-token-2022", "thiserror", ] @@ -4724,20 +4595,6 @@ dependencies = [ "syn 2.0.43", ] -[[package]] -name = "spl-tlv-account-resolution" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "062e148d3eab7b165582757453632ffeef490c02c86a48bfdb4988f63eefb3b9" -dependencies = [ - "bytemuck", - "solana-program", - "spl-discriminator", - "spl-pod", - "spl-program-error", - "spl-type-length-value", -] - [[package]] name = "spl-tlv-account-resolution" version = "0.5.0" @@ -4767,28 +4624,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "spl-token-2022" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4abf34a65ba420584a0c35f3903f8d727d1f13ababbdc3f714c6b065a686e86" -dependencies = [ - "arrayref", - "bytemuck", - "num-derive 0.4.1", - "num-traits", - "num_enum 0.7.1", - "solana-program", - "solana-zk-token-sdk", - "spl-memo", - "spl-pod", - "spl-token", - "spl-token-metadata-interface", - "spl-transfer-hook-interface 0.3.0", - "spl-type-length-value", - "thiserror", -] - [[package]] name = "spl-token-2022" version = "1.0.0" @@ -4799,7 +4634,7 @@ dependencies = [ "bytemuck", "num-derive 0.4.1", "num-traits", - "num_enum 0.7.1", + "num_enum 0.7.2", "solana-program", "solana-security-txt", "solana-zk-token-sdk", @@ -4808,7 +4643,7 @@ dependencies = [ "spl-token", "spl-token-group-interface", "spl-token-metadata-interface", - "spl-transfer-hook-interface 0.4.1", + "spl-transfer-hook-interface", "spl-type-length-value", "thiserror", ] @@ -4840,22 +4675,6 @@ dependencies = [ "spl-type-length-value", ] -[[package]] -name = "spl-transfer-hook-interface" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "051d31803f873cabe71aec3c1b849f35248beae5d19a347d93a5c9cccc5d5a9b" -dependencies = [ - "arrayref", - "bytemuck", - "solana-program", - "spl-discriminator", - "spl-pod", - "spl-program-error", - "spl-tlv-account-resolution 0.4.0", - "spl-type-length-value", -] - [[package]] name = "spl-transfer-hook-interface" version = "0.4.1" @@ -4868,7 +4687,7 @@ dependencies = [ "spl-discriminator", "spl-pod", "spl-program-error", - "spl-tlv-account-resolution 0.5.0", + "spl-tlv-account-resolution", "spl-type-length-value", ] @@ -4918,7 +4737,7 @@ version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ - "heck 0.4.1", + "heck", "proc-macro2", "quote", "rustversion", @@ -5466,12 +5285,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-segmentation" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" - [[package]] name = "unicode-width" version = "0.1.11" diff --git a/Cargo.toml b/Cargo.toml index 54dfdfa..145f901 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,17 +1,33 @@ -[workspace] -members = [ - "programs/*" -] +[package] +name = "ore" +version = "0.1.0" +description = "Ore is a cryptocurrency everyone can mine" +edition = "2021" +license = "Apache 2.0" +homepage = "" +repository = "" +documentation = "" +readme = "./README.md" +keywords = ["solana"] -# [package.metadata.kani.flags] -# default-unwind = 1 +[lib] +crate-type = ["cdylib", "lib"] +name = "ore" -[profile.release] -overflow-checks = true -lto = "fat" -codegen-units = 1 +[features] +no-entrypoint = [] +default = [] -[profile.release.build-override] -opt-level = 3 -incremental = false -codegen-units = 1 +[dependencies] +bytemuck = "1.14.3" +num_enum = "0.7.2" +shank = "0.3.0" +solana-program = "^1.16" +spl-token = { version = "^4", features = ["no-entrypoint"] } +spl-associated-token-account = { version = "^2.2", features = [ "no-entrypoint" ] } +static_assertions = "1.1.0" + +[dev-dependencies] +solana-program-test = "^1.16" +solana-sdk = "^1.16" +tokio = { version = "1.35", features = ["full"] } diff --git a/programs/ore/Cargo.toml b/programs/ore/Cargo.toml deleted file mode 100644 index 2dca00d..0000000 --- a/programs/ore/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "ore" -version = "0.1.0" -description = "Ore is a cryptocurrency everyone can mine" -edition = "2021" -license = "Apache 2.0" -homepage = "" -repository = "" -documentation = "" -readme = "./README.md" -keywords = ["solana"] - -[lib] -crate-type = ["cdylib", "lib"] -name = "ore" - -[features] -no-entrypoint = [] -no-idl = [] -no-log-ix-name = [] -cpi = ["no-entrypoint"] -default = [] - -[dependencies] -anchor-lang = "0.29.0" -anchor-spl = { version = "0.29.0", features = ["token"] } -bincode = "1.3.3" -solana-program = "^1.16" -static_assertions = "1.1.0" - -[dev-dependencies] -solana-program-test = "^1.16" diff --git a/programs/ore/Xargo.toml b/programs/ore/Xargo.toml deleted file mode 100644 index 475fb71..0000000 --- a/programs/ore/Xargo.toml +++ /dev/null @@ -1,2 +0,0 @@ -[target.bpfel-unknown-unknown.dependencies.std] -features = [] diff --git a/programs/ore/src/lib.rs b/programs/ore/src/lib.rs deleted file mode 100644 index fba7cc4..0000000 --- a/programs/ore/src/lib.rs +++ /dev/null @@ -1,795 +0,0 @@ -use std::mem::size_of; - -use anchor_lang::{ - prelude::*, - solana_program::{ - keccak::{hashv, Hash}, - pubkey, - slot_hashes::SlotHash, - system_program, sysvar, - }, -}; -use anchor_spl::{ - associated_token, - token::{self, Mint, MintTo, TokenAccount}, -}; - -// TODO Test admin and difficulty adjustment functions! - -declare_id!("CeJShZEAzBLwtcLQvbZc7UT38e4nUTn63Za5UFyYYDTS"); - -// TODO Set this before deployment -/// The unix timestamp after which mining is allowed. -pub const START_AT: i64 = 0; - -/// The initial reward rate to payout in the first epoch. -pub const INITIAL_REWARD_RATE: u64 = 10u64.pow(3u32); - -/// The initial hashing difficulty. The admin authority can update this in the future, if needed. -pub const INITIAL_DIFFICULTY: Hash = Hash::new_from_array([ - 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, -]); - -/// The mint address of the ORE token. -pub const TOKEN_MINT_ADDRESS: Pubkey = pubkey!("37TDfMS8NHpyhyCXBrY9m7rRrtj1f7TrFzD1iXqmTeUX"); - -/// The decimal precision of the ORE token. -/// Using SI prefixes, the smallest indivisible unit of ORE is a nanoORE. -/// 1 nanoORE = 0.000000001 ORE = one billionth of an ORE -pub const TOKEN_DECIMALS: u8 = 9; - -/// One ORE token, denominated in units of nanoORE. -pub const ONE_ORE: u64 = 10u64.pow(TOKEN_DECIMALS as u32); - -/// The duration of an epoch, in units of seconds. -pub const EPOCH_DURATION: i64 = 60; - -/// The target quantity of ORE to be mined per epoch, in units of nanoORE. -/// Inflation rate ≈ 1 ORE / epoch (min 0, max 2) -pub const TARGET_EPOCH_REWARDS: u64 = ONE_ORE; - -/// The maximum quantity of ORE that can be mined per epoch, in units of nanoORE. -pub const MAX_EPOCH_REWARDS: u64 = ONE_ORE.saturating_mul(2); - -/// The quantity of ORE each bus is allowed to issue per epoch. -pub const BUS_EPOCH_REWARDS: u64 = MAX_EPOCH_REWARDS.saturating_div(BUS_COUNT as u64); - -/// The number of bus accounts, for parallelizing mine operations. -pub const BUS_COUNT: u8 = 8; - -/// The smoothing factor for reward rate changes. The reward rate cannot change by more or less -/// than factor of this constant from one epoch to the next. -pub const SMOOTHING_FACTOR: u64 = 2; - -// Assert MAX_EPOCH_REWARDS is evenly divisible by BUS_COUNT. -static_assertions::const_assert!( - (MAX_EPOCH_REWARDS / BUS_COUNT as u64) * BUS_COUNT as u64 == MAX_EPOCH_REWARDS -); - -#[program] -mod ore { - use super::*; - - /// Initializes the treasury account. Can only be invoked once. - pub fn initialize_treasury(ctx: Context) -> Result<()> { - ctx.accounts.treasury.bump = ctx.bumps.treasury; - ctx.accounts.treasury.admin = ctx.accounts.signer.key(); - ctx.accounts.treasury.difficulty = INITIAL_DIFFICULTY; - ctx.accounts.treasury.reward_rate = INITIAL_REWARD_RATE; - Ok(()) - } - - /// Initializes the bus accounts. Can only be invoked once. - pub fn initialize_busses(ctx: Context) -> Result<()> { - ctx.accounts.bus_0.bump = ctx.bumps.bus_0; - ctx.accounts.bus_0.id = 0; - ctx.accounts.bus_1.bump = ctx.bumps.bus_1; - ctx.accounts.bus_1.id = 1; - ctx.accounts.bus_2.bump = ctx.bumps.bus_2; - ctx.accounts.bus_2.id = 2; - ctx.accounts.bus_3.bump = ctx.bumps.bus_3; - ctx.accounts.bus_3.id = 3; - ctx.accounts.bus_4.bump = ctx.bumps.bus_4; - ctx.accounts.bus_4.id = 4; - ctx.accounts.bus_5.bump = ctx.bumps.bus_5; - ctx.accounts.bus_5.id = 5; - ctx.accounts.bus_6.bump = ctx.bumps.bus_6; - ctx.accounts.bus_6.id = 6; - ctx.accounts.bus_7.bump = ctx.bumps.bus_7; - ctx.accounts.bus_7.id = 7; - Ok(()) - } - - /// Initializes a proof account for a new miner. Can only invoked once per signer. - pub fn initialize_proof(ctx: Context) -> Result<()> { - ctx.accounts.proof.authority = ctx.accounts.signer.key(); - ctx.accounts.proof.bump = ctx.bumps.proof; - ctx.accounts.proof.hash = hashv(&[&ctx.accounts.signer.key().to_bytes()]); - Ok(()) - } - - /// Updates the reward rate and starts the new epoch. - pub fn reset_epoch(ctx: Context) -> Result<()> { - // Validate epoch has ended. - let clock = Clock::get().unwrap(); - let treasury = &mut ctx.accounts.treasury; - let epoch_end_at = treasury.epoch_start_at.saturating_add(EPOCH_DURATION); - require!( - clock.unix_timestamp.ge(&epoch_end_at), - ProgramError::ClockInvalid - ); - - // Calculate total rewards issued during the epoch. - let bus_0 = &mut ctx.accounts.bus_0; - let bus_1 = &mut ctx.accounts.bus_1; - let bus_2 = &mut ctx.accounts.bus_2; - let bus_3 = &mut ctx.accounts.bus_3; - let bus_4 = &mut ctx.accounts.bus_4; - let bus_5 = &mut ctx.accounts.bus_5; - let bus_6 = &mut ctx.accounts.bus_6; - let bus_7 = &mut ctx.accounts.bus_7; - let total_available_rewards = bus_0 - .available_rewards - .saturating_add(bus_1.available_rewards) - .saturating_add(bus_2.available_rewards) - .saturating_add(bus_3.available_rewards) - .saturating_add(bus_4.available_rewards) - .saturating_add(bus_5.available_rewards) - .saturating_add(bus_6.available_rewards) - .saturating_add(bus_7.available_rewards); - let total_epoch_rewards = MAX_EPOCH_REWARDS.saturating_sub(total_available_rewards); - - // Update the reward amount for the next epoch. - treasury.reward_rate = calculate_new_reward_rate(treasury.reward_rate, total_epoch_rewards); - treasury.epoch_start_at = clock.unix_timestamp; - - // Reset bus accounts. - bus_0.available_rewards = BUS_EPOCH_REWARDS; - bus_1.available_rewards = BUS_EPOCH_REWARDS; - bus_2.available_rewards = BUS_EPOCH_REWARDS; - bus_3.available_rewards = BUS_EPOCH_REWARDS; - bus_4.available_rewards = BUS_EPOCH_REWARDS; - bus_5.available_rewards = BUS_EPOCH_REWARDS; - bus_6.available_rewards = BUS_EPOCH_REWARDS; - bus_7.available_rewards = BUS_EPOCH_REWARDS; - - // Top up treasury token account. - token::mint_to( - CpiContext::new_with_signer( - ctx.accounts.token_program.to_account_info(), - MintTo { - authority: treasury.to_account_info(), - mint: ctx.accounts.mint.to_account_info(), - to: ctx.accounts.treasury_tokens.to_account_info(), - }, - &[&[TREASURY, &[treasury.bump]]], - ), - total_epoch_rewards, - )?; - - // Log data. - msg!("Epoch rewards: {:?}", total_epoch_rewards); - msg!("Reward rate: {:?}", treasury.reward_rate); - msg!("Supply: {:?}", ctx.accounts.mint.supply); - - Ok(()) - } - - /// Distributes Ore tokens to the signer if a valid hash is provided. - pub fn mine(ctx: Context, hash: Hash, nonce: u64) -> Result<()> { - // Validate epoch is active. - let clock = Clock::get().unwrap(); - let treasury = &mut ctx.accounts.treasury; - let epoch_end_at = treasury.epoch_start_at.saturating_add(EPOCH_DURATION); - require!( - clock.unix_timestamp.lt(&epoch_end_at), - ProgramError::EpochNeedsReset - ); - - // Validate provided hash. - let proof = &mut ctx.accounts.proof; - validate_hash( - proof.hash.clone(), - hash.clone(), - ctx.accounts.signer.key(), - nonce, - treasury.difficulty, - )?; - - // Update claimable rewards. - let bus = &mut ctx.accounts.bus; - require!( - bus.available_rewards.ge(&treasury.reward_rate), - ProgramError::BusInsufficientFunds - ); - bus.available_rewards = bus.available_rewards.saturating_sub(treasury.reward_rate); - proof.claimable_rewards = proof.claimable_rewards.saturating_add(treasury.reward_rate); - - // Hash most recent slot hash into the next challenge to prevent pre-mining attacks. - let slot_hash_bytes = &ctx.accounts.slot_hashes.data.borrow()[0..size_of::()]; - let slot_hash: SlotHash = bincode::deserialize(slot_hash_bytes).unwrap(); - proof.hash = hashv(&[hash.as_ref(), slot_hash.1.as_ref()]); - - // Update lifetime stats. - proof.total_hashes = proof.total_hashes.saturating_add(1); - proof.total_rewards = proof.total_rewards.saturating_add(1); - - // Log data. - msg!("Reward rate: {:?}", treasury.reward_rate); - msg!("Claimable rewards: {:?}", proof.claimable_rewards); - msg!("Total hashes: {:?}", proof.total_hashes); - msg!("Total rewards: {:?}", proof.total_rewards); - - Ok(()) - } - - pub fn claim(ctx: Context, amount: u64) -> Result<()> { - // Validate claim is for an appropriate quantity of tokens. - let proof = &mut ctx.accounts.proof; - require!( - proof.claimable_rewards.ge(&amount), - ProgramError::ClaimTooLarge - ); - - // Update claimable amount. - proof.claimable_rewards = proof.claimable_rewards.saturating_sub(amount); - - // Update lifetime status. - let treasury = &mut ctx.accounts.treasury; - treasury.total_claimed_rewards = treasury.total_claimed_rewards.saturating_add(amount); - - // Distribute tokens from treasury to beneficiary. - let treasury_tokens = &ctx.accounts.treasury_tokens; - require!( - treasury_tokens.amount.ge(&amount), - ProgramError::TreasuryInsufficientFunds - ); - token::transfer( - CpiContext::new_with_signer( - ctx.accounts.token_program.to_account_info(), - token::Transfer { - from: treasury_tokens.to_account_info(), - to: ctx.accounts.beneficiary.to_account_info(), - authority: treasury.to_account_info(), - }, - &[&[TREASURY, &[treasury.bump]]], - ), - amount, - )?; - - // Log data. - msg!("Claimable rewards: {:?}", proof.claimable_rewards); - msg!( - "Total claimed rewards: {:?}", - treasury.total_claimed_rewards - ); - - Ok(()) - } - - /// Updates the admin to a new value. Can only be invoked by the admin authority. - pub fn update_admin(ctx: Context, new_admin: Pubkey) -> Result<()> { - ctx.accounts.treasury.admin = new_admin; - Ok(()) - } - - /// Updates the difficulty to a new value. Can only be invoked by the admin authority. - /// - /// Ore subdivides into 1B units of indivisible nanoORE. If global hashpower increases to the - /// point where >1B valid hashes are being submitted per epoch, the Ore inflation rate could - /// be pushed steadily above 1 ORE/epoch. The protocol guarantees inflation can never exceed - /// 2 ORE/epoch, but it is the responsibility of the admin to adjust the mining difficulty - /// as needed to maintain the 1 ORE/epoch average. - /// - /// It is worth noting that Solana today processes well below 1M real TPS or - /// (60 * 1,000,000) = 60,000,000 hashes per epoch. Even if every transaction on the network - /// were mine operation, this is still two orders of magnitude below the threshold where the - /// Ore inflation rate would be challenged. So in practice, Solana is likely to reach its - /// network saturation point long before the Ore inflation hits its boundary condition. - pub fn update_difficulty(ctx: Context, new_difficulty: Hash) -> Result<()> { - ctx.accounts.treasury.difficulty = new_difficulty; - Ok(()) - } -} - -fn validate_hash( - current_hash: Hash, - hash: Hash, - signer: Pubkey, - nonce: u64, - difficulty: Hash, -) -> Result<()> { - // Validate hash correctness. - let hash_ = hashv(&[ - current_hash.as_ref(), - signer.as_ref(), - nonce.to_be_bytes().as_slice(), - ]); - require!(hash.eq(&hash_), ProgramError::HashInvalid); - - // Validate hash difficulty. - require!(hash.le(&difficulty), ProgramError::HashInvalid); - - Ok(()) -} - -fn calculate_new_reward_rate(current_rate: u64, epoch_rewards: u64) -> u64 { - // Avoid division by zero. Leave the reward rate unchanged. - if epoch_rewards.eq(&0) { - return current_rate; - } - - // Calculate new reward rate. - let new_rate = (current_rate as u128) - .saturating_mul(TARGET_EPOCH_REWARDS as u128) - .saturating_div(epoch_rewards as u128) as u64; - - // Smooth reward rate to not change by more than a constant factor from one epoch to the next. - let new_rate_min = current_rate.saturating_div(SMOOTHING_FACTOR); - let new_rate_max = current_rate.saturating_mul(SMOOTHING_FACTOR); - let new_rate_smoothed = new_rate_min.max(new_rate_max.min(new_rate)); - - // Prevent reward rate from dropping below 1 or exceeding BUS_EPOCH_REWARDS and return. - new_rate_smoothed.max(1).min(BUS_EPOCH_REWARDS) -} - -/// The seed of the bus account PDA. -pub const BUS: &[u8] = b"bus"; - -/// The seed of the proof account PDA. -pub const PROOF: &[u8] = b"proof"; - -/// The seed of the treasury account PDA. -pub const TREASURY: &[u8] = b"treasury"; - -/// Bus is an account type used to track the number of processed hashes and issued rewards -/// during an epoch. There are 8 bus accounts to provide sufficient parallelism for mine ops -/// and reduce write lock contention. -#[account] -#[derive(Debug, PartialEq)] -pub struct Bus { - /// The bump of the bus account PDA. - pub bump: u8, - - /// The ID of the bus account. - pub id: u8, - - /// The quantity of rewards this bus can issue in the current epoch epoch. - pub available_rewards: u64, -} - -/// Proof is an account type used to track a miner's hash chain. -#[account] -#[derive(Debug, PartialEq)] -pub struct Proof { - /// The bump of the proof account PDA. - pub bump: u8, - - /// The account (i.e. miner) authorized to use this proof. - pub authority: Pubkey, - - /// The quantity of tokens this miner may claim from the treasury. - pub claimable_rewards: u64, - - /// The proof's current hash. - pub hash: Hash, - - /// The total lifetime hashes provided by this miner. - pub total_hashes: u64, - - /// The total lifetime rewards distributed to this miner. - pub total_rewards: u64, -} - -/// Treasury is an account type used to track global program variables. -#[account] -#[derive(Debug, PartialEq)] -pub struct Treasury { - /// The bump of the treasury account PDA. - pub bump: u8, - - /// The admin authority with permission to update the difficulty. - pub admin: Pubkey, - - /// The hash difficulty. - pub difficulty: Hash, - - /// The timestamp of the start of the current epoch. - pub epoch_start_at: i64, - - /// The reward rate to payout to miners for submiting valid hashes. - pub reward_rate: u64, - - /// The total lifetime claimed rewards. - pub total_claimed_rewards: u64, -} - -#[derive(Accounts)] -pub struct InitializeTreasury<'info> { - /// The signer of the transaction. - #[account(mut)] - pub signer: Signer<'info>, - - /// The Ore token mint. - #[account(init, address = TOKEN_MINT_ADDRESS, payer = signer, mint::decimals = TOKEN_DECIMALS, mint::authority = treasury)] - pub mint: Account<'info, Mint>, - - /// The treasury account. - #[account(init, seeds = [TREASURY], bump, payer = signer, space = 8 + size_of::())] - pub treasury: Account<'info, Treasury>, - - /// The treasury token account. - #[account(init, associated_token::mint = mint, associated_token::authority = treasury, payer = signer)] - pub treasury_tokens: Account<'info, TokenAccount>, - - /// The Solana system program. - #[account(address = system_program::ID)] - pub system_program: Program<'info, System>, - - /// The SPL token program. - #[account(address = anchor_spl::token::ID)] - pub token_program: Program<'info, token::Token>, - - /// The SPL associated token program. - #[account(address = anchor_spl::associated_token::ID)] - pub associated_token_program: Program<'info, associated_token::AssociatedToken>, - - /// The rent sysvar account. - #[account(address = sysvar::rent::ID)] - pub rent: Sysvar<'info, Rent>, -} - -#[derive(Accounts)] -pub struct InitializeBusses<'info> { - /// The signer of the transaction. - #[account(mut)] - pub signer: Signer<'info>, - - /// The treasury account. - #[account(seeds = [TREASURY], bump = treasury.bump)] - pub treasury: Account<'info, Treasury>, - - /// The Ore token mint account. - #[account(address = TOKEN_MINT_ADDRESS)] - pub mint: Account<'info, Mint>, - - /// Bus account 0. - #[account(init, seeds = [BUS, &[0]], bump, payer = signer, space = 8 + size_of::())] - pub bus_0: Account<'info, Bus>, - - /// Bus account 1. - #[account(init, seeds = [BUS, &[1]], bump, payer = signer, space = 8 + size_of::())] - pub bus_1: Account<'info, Bus>, - - /// Bus account 2. - #[account(init, seeds = [BUS, &[2]], bump, payer = signer, space = 8 + size_of::())] - pub bus_2: Account<'info, Bus>, - - /// Bus account 3. - #[account(init, seeds = [BUS, &[3]], bump, payer = signer, space = 8 + size_of::())] - pub bus_3: Account<'info, Bus>, - - /// Bus account 4. - #[account(init, seeds = [BUS, &[4]], bump, payer = signer, space = 8 + size_of::())] - pub bus_4: Account<'info, Bus>, - - /// Bus account 5. - #[account(init, seeds = [BUS, &[5]], bump, payer = signer, space = 8 + size_of::())] - pub bus_5: Account<'info, Bus>, - - /// Bus account 6. - #[account(init, seeds = [BUS, &[6]], bump, payer = signer, space = 8 + size_of::())] - pub bus_6: Account<'info, Bus>, - - /// Bus account 7. - #[account(init, seeds = [BUS, &[7]], bump, payer = signer, space = 8 + size_of::())] - pub bus_7: Account<'info, Bus>, - - /// The Solana system program. - #[account(address = system_program::ID)] - pub system_program: Program<'info, System>, -} - -/// InitializeProof initializes a new proof account for a miner. -#[derive(Accounts)] -pub struct InitializeProof<'info> { - /// The signer of the transaction. - #[account(mut)] - pub signer: Signer<'info>, - - /// The proof account. - #[account(init, seeds = [PROOF, signer.key().as_ref()], bump, payer = signer, space = 8 + size_of::())] - pub proof: Account<'info, Proof>, - - /// The Solana system program. - #[account(address = system_program::ID)] - pub system_program: Program<'info, System>, -} - -// ResetEpoch adjusts the reward rate based on global hashpower and begins the new epoch. -#[derive(Accounts)] -pub struct ResetEpoch<'info> { - /// The signer of the transaction. - #[account(mut)] - pub signer: Signer<'info>, - - /// Bus account 0. - #[account(mut, seeds = [BUS, &[0]], bump = bus_0.bump)] - pub bus_0: Box>, - - /// Bus account 1. - #[account(mut, seeds = [BUS, &[1]], bump = bus_1.bump)] - pub bus_1: Box>, - - /// Bus account 2. - #[account(mut, seeds = [BUS, &[2]], bump = bus_2.bump)] - pub bus_2: Box>, - - /// Bus account 3. - #[account(mut, seeds = [BUS, &[3]], bump = bus_3.bump)] - pub bus_3: Box>, - - /// Bus account 4. - #[account(mut, seeds = [BUS, &[4]], bump = bus_4.bump)] - pub bus_4: Box>, - - /// Bus account 5. - #[account(mut, seeds = [BUS, &[5]], bump = bus_5.bump)] - pub bus_5: Box>, - - /// Bus account 6. - #[account(mut, seeds = [BUS, &[6]], bump = bus_6.bump)] - pub bus_6: Box>, - - /// Bus account 7. - #[account(mut, seeds = [BUS, &[7]], bump = bus_7.bump)] - pub bus_7: Box>, - - /// The Ore token mint account. - #[account(mut, address = TOKEN_MINT_ADDRESS)] - pub mint: Account<'info, Mint>, - - /// The treasury account. - #[account(mut, seeds = [TREASURY], bump = treasury.bump)] - pub treasury: Account<'info, Treasury>, - - /// The treasury token account. - #[account(mut, associated_token::mint = mint, associated_token::authority = treasury)] - pub treasury_tokens: Account<'info, TokenAccount>, - - /// The SPL token program. - #[account(address = anchor_spl::token::ID)] - pub token_program: Program<'info, token::Token>, -} - -/// Mine distributes Ore to the beneficiary if the signer provides a valid hash. -#[derive(Accounts)] -#[instruction(hash: Hash, nonce: u64)] -pub struct Mine<'info> { - /// The signer of the transaction (i.e. the miner). - #[account(mut, address = proof.authority)] - pub signer: Signer<'info>, - - /// A bus account. - #[account(mut, constraint = bus.id.lt(&BUS_COUNT) @ ProgramError::BusInvalid)] - pub bus: Account<'info, Bus>, - - /// The proof account. - #[account(mut, seeds = [PROOF, signer.key().as_ref()], bump = proof.bump)] - pub proof: Account<'info, Proof>, - - /// The treasury account. - #[account(seeds = [TREASURY], bump = treasury.bump)] - pub treasury: Account<'info, Treasury>, - - /// The SPL token program. - #[account(address = anchor_spl::token::ID)] - pub token_program: Program<'info, token::Token>, - - /// The slot hashes sysvar account. - /// CHECK: SlotHashes is too large to deserialize. Instead we manually verify the sysvar address and deserialize only the slice we need. - #[account(address = sysvar::slot_hashes::ID)] - pub slot_hashes: AccountInfo<'info>, -} - -#[derive(Accounts)] -#[instruction(amount: u64)] -pub struct Claim<'info> { - /// The signer of the transaction (i.e. the miner). - #[account(mut, address = proof.authority)] - pub signer: Signer<'info>, - - /// The beneficiary token account to distribute rewards to. - #[account(mut, token::mint = mint)] - pub beneficiary: Account<'info, TokenAccount>, - - /// The Ore token mint account. - #[account(address = TOKEN_MINT_ADDRESS)] - pub mint: Account<'info, Mint>, - - /// The proof account. - #[account(mut, seeds = [PROOF, signer.key().as_ref()], bump = proof.bump)] - pub proof: Account<'info, Proof>, - - /// The treasury account. - #[account(mut, seeds = [TREASURY], bump = treasury.bump)] - pub treasury: Account<'info, Treasury>, - - /// The treasury token account. - #[account(mut, associated_token::mint = mint, associated_token::authority = treasury)] - pub treasury_tokens: Account<'info, TokenAccount>, - - /// The SPL token program. - #[account(address = anchor_spl::token::ID)] - pub token_program: Program<'info, token::Token>, -} - -/// UpdateAdmin allows the admin to reassign the admin authority. -#[derive(Accounts)] -#[instruction(new_admin: Pubkey)] -pub struct UpdateAdmin<'info> { - /// The signer of the transaction (i.e. the admin). - #[account(mut)] - pub signer: Signer<'info>, - - /// The treasury account. - #[account(mut, seeds = [TREASURY], bump = treasury.bump, constraint = treasury.admin.eq(&signer.key()) @ ProgramError::NotAuthorized)] - pub treasury: Account<'info, Treasury>, -} - -/// UpdateDifficulty allows the admin to update the mining difficulty. -#[derive(Accounts)] -#[instruction(new_difficulty: Hash)] -pub struct UpdateDifficulty<'info> { - /// The signer of the transaction (i.e. the admin). - #[account(mut)] - pub signer: Signer<'info>, - - /// The treasury account. - #[account(mut, seeds = [TREASURY], bump = treasury.bump, constraint = treasury.admin.eq(&signer.key()) @ ProgramError::NotAuthorized)] - pub treasury: Account<'info, Treasury>, -} - -/// MineEvent logs revelant data about a successful Ore mining transaction. -#[event] -#[derive(Debug)] -pub struct MineEvent { - /// The signer of the transaction (i.e. the miner). - pub signer: Pubkey, - - /// The beneficiary token account to which rewards were minted. - pub beneficiary: Pubkey, - - /// The quantity of new Ore tokens that were mined. - pub reward: u64, - - /// The current Ore token supply. - pub supply: u64, - - /// The current mining difficulty. - pub difficulty: Hash, - - /// The valid hash provided by the signer. - pub hash: Hash, - - /// The nonce provided by the signer. - pub nonce: u64, -} - -#[error_code] -pub enum ProgramError { - #[msg("The clock time is invalid")] - ClockInvalid, - #[msg("The hash is invalid")] - HashInvalid, - #[msg("Mining has not started yet")] - NotStarted, - #[msg("The epoch has ended and needs to be reset")] - EpochNeedsReset, - #[msg("An invalid bus account was provided")] - BusInvalid, - #[msg("This bus does not have enough tokens to pay the reward")] - BusInsufficientFunds, - #[msg("The signer is not authorized to perform this action")] - NotAuthorized, - #[msg("You cannot claim more tokens than are available")] - ClaimTooLarge, - #[msg("The treasury does not have enough tokens to honor the claim")] - TreasuryInsufficientFunds, -} - -#[cfg(test)] -mod tests { - use anchor_lang::{ - prelude::Pubkey, - solana_program::keccak::{hashv, Hash}, - }; - - use crate::{calculate_new_reward_rate, validate_hash, SMOOTHING_FACTOR, TARGET_EPOCH_REWARDS}; - - #[test] - fn test_validate_hash_pass() { - let h1 = Hash::new_from_array([1; 32]); - let signer = Pubkey::new_unique(); - let nonce = 10u64; - let difficulty = Hash::new_from_array([255; 32]); - let h2 = hashv(&[ - h1.to_bytes().as_slice(), - signer.to_bytes().as_slice(), - nonce.to_be_bytes().as_slice(), - ]); - let res = validate_hash(h1, h2, signer, nonce, difficulty); - assert!(res.is_ok()); - } - - #[test] - fn test_validate_hash_fail() { - let h1 = Hash::new_from_array([1; 32]); - let signer = Pubkey::new_unique(); - let nonce = 10u64; - let difficulty = Hash::new_from_array([255; 32]); - let h2 = Hash::new_from_array([2; 32]); - let res = validate_hash(h1, h2, signer, nonce, difficulty); - assert!(res.is_err()); - } - - #[test] - fn test_validate_hash_fail_difficulty() { - let h1 = Hash::new_from_array([1; 32]); - let signer = Pubkey::new_unique(); - let nonce = 10u64; - let difficulty = Hash::new_from_array([0; 32]); - let h2 = hashv(&[ - h1.to_bytes().as_slice(), - signer.to_bytes().as_slice(), - nonce.to_be_bytes().as_slice(), - ]); - let res = validate_hash(h1, h2, signer, nonce, difficulty); - assert!(res.is_err()); - } - - #[test] - fn test_calculate_new_reward_rate_stable() { - let current_rate = 1000; - let new_rate = calculate_new_reward_rate(current_rate, TARGET_EPOCH_REWARDS); - assert!(new_rate.eq(¤t_rate)); - } - - #[test] - fn test_calculate_new_reward_rate_no_chage() { - let current_rate = 1000; - let new_rate = calculate_new_reward_rate(current_rate, 0); - assert!(new_rate.eq(¤t_rate)); - } - - #[test] - fn test_calculate_new_reward_rate_lower() { - let current_rate = 1000; - let new_rate = - calculate_new_reward_rate(current_rate, TARGET_EPOCH_REWARDS.saturating_add(1_000_000)); - assert!(new_rate.lt(¤t_rate)); - } - - #[test] - fn test_calculate_new_reward_rate_higher() { - let current_rate = 1000; - let new_rate = - calculate_new_reward_rate(current_rate, TARGET_EPOCH_REWARDS.saturating_sub(1_000_000)); - assert!(new_rate.gt(¤t_rate)); - } - - #[test] - fn test_calculate_new_reward_rate_max_smooth() { - let current_rate = 1000; - let new_rate = calculate_new_reward_rate(current_rate, 1); - assert!(new_rate.eq(¤t_rate.saturating_mul(SMOOTHING_FACTOR))); - } - - #[test] - fn test_calculate_new_reward_rate_min_smooth() { - let current_rate = 1000; - let new_rate = calculate_new_reward_rate(current_rate, u64::MAX); - assert!(new_rate.eq(¤t_rate.saturating_div(SMOOTHING_FACTOR))); - } -} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 4b689af..5e4cacf 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,6 +1,5 @@ [toolchain] channel = "1.70.0" -# channel = "nightly" components = [ "rustfmt", "rust-analyzer" ] -profile = "minimal" targets = [ "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "aarch64-apple-darwin"] +profile = "minimal" diff --git a/src/instruction.rs b/src/instruction.rs new file mode 100644 index 0000000..c46ab97 --- /dev/null +++ b/src/instruction.rs @@ -0,0 +1,103 @@ +use bytemuck::{Pod, Zeroable}; +use num_enum::TryFromPrimitive; +use shank::ShankInstruction; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, ShankInstruction, TryFromPrimitive)] +#[rustfmt::skip] +pub enum OreInstruction { + #[account(0, name = "ore_program", desc = "Ore program")] + #[account(1, name = "signer", desc = "Signer", signer)] + #[account(2, name = "bus_0", desc = "Ore bus account 0", writable)] + #[account(3, name = "bus_1", desc = "Ore bus account 1", writable)] + #[account(4, name = "bus_2", desc = "Ore bus account 2", writable)] + #[account(5, name = "bus_3", desc = "Ore bus account 3", writable)] + #[account(6, name = "bus_4", desc = "Ore bus account 4", writable)] + #[account(7, name = "bus_5", desc = "Ore bus account 5", writable)] + #[account(8, name = "bus_6", desc = "Ore bus account 6", writable)] + #[account(9, name = "bus_7", desc = "Ore bus account 7", writable)] + #[account(10, name = "mint", desc = "Ore token mint account", writable)] + #[account(11, name = "treasury", desc = "Ore treasury account", writable)] + #[account(12, name = "treasury_tokens", desc = "Ore treasury token account", writable)] + #[account(13, name = "token_program", desc = "SPL token program")] + Epoch = 0, + + #[account(0, name = "ore_program", desc = "Ore program")] + #[account(1, name = "signer", desc = "Signer", signer)] + #[account(2, name = "proof", desc = "Ore miner proof account", writable)] + #[account(3, name = "system_program", desc = "Solana system program")] + Proof = 1, + + #[account(0, name = "ore_program", desc = "Ore program")] + #[account(1, name = "signer", desc = "Signer", signer)] + #[account(2, name = "bus", desc = "Ore bus account", writable)] + #[account(3, name = "proof", desc = "Ore miner proof account", writable)] + #[account(4, name = "treasury", desc = "Ore treasury account")] + #[account(5, name = "token_program", desc = "SPL token program")] + #[account(6, name = "slot_hashes", desc = "Solana slot hashes sysvar")] + Mine = 2, + + #[account(0, name = "ore_program", desc = "Ore program")] + #[account(1, name = "signer", desc = "Signer", signer)] + #[account(2, name = "beneficiary", desc = "Beneficiary token account", writable)] + #[account(3, name = "mint", desc = "Ore token mint account")] + #[account(4, name = "proof", desc = "Ore miner proof account", writable)] + #[account(5, name = "treasury", desc = "Ore treasury account", writable)] + #[account(6, name = "treasury_tokens", desc = "Ore treasury token account", writable)] + #[account(7, name = "token_program", desc = "SPL token program")] + Claim = 3, + + #[account(0, name = "ore_program", desc = "Ore program")] + #[account(1, name = "admin", desc = "Admin signer", signer)] + #[account(2, name = "bus_0", desc = "Ore bus account 0", writable)] + #[account(3, name = "bus_1", desc = "Ore bus account 1", writable)] + #[account(4, name = "bus_2", desc = "Ore bus account 2", writable)] + #[account(5, name = "bus_3", desc = "Ore bus account 3", writable)] + #[account(6, name = "bus_4", desc = "Ore bus account 4", writable)] + #[account(7, name = "bus_5", desc = "Ore bus account 5", writable)] + #[account(8, name = "bus_6", desc = "Ore bus account 6", writable)] + #[account(9, name = "bus_7", desc = "Ore bus account 7", writable)] + #[account(10, name = "mint", desc = "Ore token mint account")] + #[account(11, name = "treasury", desc = "Ore treasury account")] + #[account(12, name = "treasury_tokens", desc = "Ore treasury token account", writable)] + #[account(13, name = "system_program", desc = "Solana system program")] + #[account(14, name = "token_program", desc = "SPL token program")] + #[account(15, name = "rent", desc = "Solana rent sysvar")] + Initialize = 100, + // #[account(15, name = "associated_token_program", desc = "SPL associated token program")] + + // TODO + // #[account(0, name = "ore_program", desc = "Ore program")] + // UpdateAdmin = 102, + + // TODO + // #[account(0, name = "ore_program", desc = "Ore program")] + // UpdateDifficulty = 103, +} + +impl OreInstruction { + pub fn to_vec(&self) -> Vec { + vec![*self as u8] + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct InitializeArgs { + pub bus_0_bump: u8, + pub bus_1_bump: u8, + pub bus_2_bump: u8, + pub bus_3_bump: u8, + pub bus_4_bump: u8, + pub bus_5_bump: u8, + pub bus_6_bump: u8, + pub bus_7_bump: u8, + pub mint_bump: u8, + pub treasury_bump: u8, +} + +impl InitializeArgs { + pub fn to_bytes(&self) -> &[u8] { + bytemuck::bytes_of(self) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7f73a30 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,514 @@ +pub mod instruction; +mod loaders; + +use std::mem::size_of; + +use bytemuck::{Pod, Zeroable}; +use solana_program::program_pack::Pack; +use solana_program::{self, sysvar}; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + declare_id, + entrypoint::ProgramResult, + keccak::{hashv, Hash}, + program_error::ProgramError, + pubkey, + pubkey::Pubkey, + rent::Rent, + system_program, + sysvar::Sysvar, +}; + +use instruction::*; +use loaders::*; +use spl_token::state::Mint; + +// TODO Test admin and difficulty adjustment functions +// TODO Use more decimals? + +declare_id!("CeJShZEAzBLwtcLQvbZc7UT38e4nUTn63Za5UFyYYDTS"); + +#[cfg(not(feature = "no-entrypoint"))] +solana_program::entrypoint!(process_instruction); + +// TODO Set this before deployment +/// The unix timestamp after which mining is allowed. +pub const START_AT: i64 = 0; + +/// The initial reward rate to payout in the first epoch. +pub const INITIAL_REWARD_RATE: u64 = 10u64.pow(3u32); + +/// The initial hashing difficulty. The admin authority can update this in the future, if needed. +pub const INITIAL_DIFFICULTY: Hash = Hash::new_from_array([ + 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, +]); + +/// The mint address of the ORE token. +pub const MINT_ADDRESS: Pubkey = pubkey!("37TDfMS8NHpyhyCXBrY9m7rRrtj1f7TrFzD1iXqmTeUX"); + +/// The decimal precision of the ORE token. +/// Using SI prefixes, the smallest indivisible unit of ORE is a nanoORE. +/// 1 nanoORE = 0.000000001 ORE = one billionth of an ORE +pub const TOKEN_DECIMALS: u8 = 9; + +/// One ORE token, denominated in units of nanoORE. +pub const ONE_ORE: u64 = 10u64.pow(TOKEN_DECIMALS as u32); + +/// The duration of an epoch, in units of seconds. +pub const EPOCH_DURATION: i64 = 60; + +/// The target quantity of ORE to be mined per epoch, in units of nanoORE. +/// Inflation rate ≈ 1 ORE / epoch (min 0, max 2) +pub const TARGET_EPOCH_REWARDS: u64 = ONE_ORE; + +/// The maximum quantity of ORE that can be mined per epoch, in units of nanoORE. +pub const MAX_EPOCH_REWARDS: u64 = ONE_ORE.saturating_mul(2); + +/// The quantity of ORE each bus is allowed to issue per epoch. +pub const BUS_EPOCH_REWARDS: u64 = MAX_EPOCH_REWARDS.saturating_div(BUS_COUNT as u64); + +/// The number of bus accounts, for parallelizing mine operations. +pub const BUS_COUNT: usize = 8; + +/// The smoothing factor for reward rate changes. The reward rate cannot change by more or less +/// than factor of this constant from one epoch to the next. +pub const SMOOTHING_FACTOR: u64 = 2; + +// Assert MAX_EPOCH_REWARDS is evenly divisible by BUS_COUNT. +static_assertions::const_assert!( + (MAX_EPOCH_REWARDS / BUS_COUNT as u64) * BUS_COUNT as u64 == MAX_EPOCH_REWARDS +); + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let (tag, data) = data + .split_first() + .ok_or(ProgramError::InvalidInstructionData)?; + + let ix = OreInstruction::try_from(*tag).or(Err(ProgramError::InvalidInstructionData))?; + + match ix { + OreInstruction::Epoch => process_epoch(program_id, accounts, data)?, + OreInstruction::Proof => process_proof(program_id, accounts, data)?, + OreInstruction::Mine => process_mine(program_id, accounts, data)?, + OreInstruction::Claim => process_claim(program_id, accounts, data)?, + OreInstruction::Initialize => process_initialize(program_id, accounts, data)?, + } + + Ok(()) +} + +fn process_epoch<'a, 'info>( + _program_id: &Pubkey, + accounts: &'a [AccountInfo<'info>], + data: &[u8], +) -> ProgramResult { + // TODO + Ok(()) +} + +fn process_proof<'a, 'info>( + _program_id: &Pubkey, + accounts: &'a [AccountInfo<'info>], + data: &[u8], +) -> ProgramResult { + // TODO + Ok(()) +} + +fn process_mine<'a, 'info>( + _program_id: &Pubkey, + accounts: &'a [AccountInfo<'info>], + data: &[u8], +) -> ProgramResult { + // TODO + Ok(()) +} + +fn process_claim<'a, 'info>( + _program_id: &Pubkey, + accounts: &'a [AccountInfo<'info>], + data: &[u8], +) -> ProgramResult { + // TODO + Ok(()) +} + +fn process_initialize<'a, 'info>( + _program_id: &Pubkey, + accounts: &'a [AccountInfo<'info>], + data: &[u8], +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + let args = bytemuck::try_from_bytes::(data) + .or(Err(ProgramError::InvalidInstructionData))?; + + // Account 1: Signer + let signer = load_signer(next_account_info(accounts_iter)?)?; + + // Accounts 2-9: Busses + #[rustfmt::skip] + let busses = vec![ + load_uninitialized_pda(next_account_info(accounts_iter)?, &[BUS, &[0], &[args.bus_0_bump]])?, + load_uninitialized_pda(next_account_info(accounts_iter)?, &[BUS, &[1], &[args.bus_1_bump]])?, + load_uninitialized_pda(next_account_info(accounts_iter)?, &[BUS, &[2], &[args.bus_2_bump]])?, + load_uninitialized_pda(next_account_info(accounts_iter)?, &[BUS, &[3], &[args.bus_3_bump]])?, + load_uninitialized_pda(next_account_info(accounts_iter)?, &[BUS, &[4], &[args.bus_4_bump]])?, + load_uninitialized_pda(next_account_info(accounts_iter)?, &[BUS, &[5], &[args.bus_5_bump]])?, + load_uninitialized_pda(next_account_info(accounts_iter)?, &[BUS, &[6], &[args.bus_6_bump]])?, + load_uninitialized_pda(next_account_info(accounts_iter)?, &[BUS, &[7], &[args.bus_7_bump]])?, + ]; + + // Account 10: Mint + #[rustfmt::skip] + let mint = load_uninitialized_pda(next_account_info(accounts_iter)?, &[MINT, &[args.mint_bump]])?; + + // Account 11: Treasury + #[rustfmt::skip] + let treasury_account_info = load_uninitialized_pda(next_account_info(accounts_iter)?, &[TREASURY, &[args.treasury_bump]])?; + + // Account 12: Treasury tokens + let treasury_tokens = load_uninitialized_account(next_account_info(accounts_iter)?)?; + + // Account 13: System program + let system_program = load_account(next_account_info(accounts_iter)?, system_program::id())?; + + // Account 14: Token program + let token_program = load_account(next_account_info(accounts_iter)?, spl_token::id())?; + + // Account 15: Rent sysvar + let rent_sysvar = load_account(next_account_info(accounts_iter)?, sysvar::rent::id())?; + + // Initialize bus accounts + let bus_bumps = vec![ + args.bus_0_bump, + args.bus_1_bump, + args.bus_2_bump, + args.bus_3_bump, + args.bus_4_bump, + args.bus_5_bump, + args.bus_6_bump, + args.bus_7_bump, + ]; + for i in 0..BUS_COUNT { + create_pda( + busses[i], + &crate::id(), + size_of::(), + &[BUS, &[i as u8], &[bus_bumps[i]]], + system_program, + signer, + )?; + busses[i].try_borrow_mut_data()?.copy_from_slice( + Bus { + bump: bus_bumps[i] as u32, + id: i as u32, + available_rewards: 0, + } + .to_bytes(), + ); + } + + // Initialize treasury + create_pda( + treasury_account_info, + &crate::id(), + size_of::(), + &[TREASURY, &[args.treasury_bump]], + system_program, + signer, + )?; + let mut treasury_data = treasury_account_info.data.borrow_mut(); + let mut treasury = bytemuck::try_from_bytes_mut::(&mut treasury_data).unwrap(); + treasury.bump = args.treasury_bump as u64; + treasury.admin = *signer.key; + treasury.epoch_start_at = 0; + treasury.reward_rate = INITIAL_REWARD_RATE; + treasury.total_claimed_rewards = 0; + + // Initialize mint + create_pda( + mint, + &spl_token::id(), + Mint::LEN, + &[MINT, &[args.mint_bump]], + system_program, + signer, + )?; + solana_program::program::invoke_signed( + &spl_token::instruction::initialize_mint( + &spl_token::id(), + mint.key, + treasury_account_info.key, + None, + TOKEN_DECIMALS, + )?, + &[ + token_program.clone(), + mint.clone(), + treasury_account_info.clone(), + rent_sysvar.clone(), + ], + &[&[MINT, &[args.mint_bump]]], + )?; + + // TODO Initialize treasury token account + create_pda( + mint, + &spl_token::id(), + spl_token::state::Account::LEN, + &[MINT, &[args.mint_bump]], + system_program, + signer, + )?; + solana_program::program::invoke_signed( + &spl_token::instruction::initialize_mint( + &spl_token::id(), + mint.key, + treasury_account_info.key, + None, + TOKEN_DECIMALS, + )?, + &[ + token_program.clone(), + mint.clone(), + treasury_account_info.clone(), + rent_sysvar.clone(), + ], + &[&[MINT, &[args.mint_bump]]], + )?; + + Ok(()) +} + +fn validate_hash( + current_hash: Hash, + hash: Hash, + signer: Pubkey, + nonce: u64, + difficulty: Hash, +) -> Result<(), ProgramError> { + // Validate hash correctness. + let hash_ = hashv(&[ + current_hash.as_ref(), + signer.as_ref(), + nonce.to_be_bytes().as_slice(), + ]); + if !hash.eq(&hash_) { + return Err(ProgramError::Custom(1)); + } + + // Validate hash difficulty. + if !hash.le(&difficulty) { + return Err(ProgramError::Custom(1)); + } + + Ok(()) +} + +fn calculate_new_reward_rate(current_rate: u64, epoch_rewards: u64) -> u64 { + // Avoid division by zero. Leave the reward rate unchanged. + if epoch_rewards.eq(&0) { + return current_rate; + } + + // Calculate new reward rate. + let new_rate = (current_rate as u128) + .saturating_mul(TARGET_EPOCH_REWARDS as u128) + .saturating_div(epoch_rewards as u128) as u64; + + // Smooth reward rate to not change by more than a constant factor from one epoch to the next. + let new_rate_min = current_rate.saturating_div(SMOOTHING_FACTOR); + let new_rate_max = current_rate.saturating_mul(SMOOTHING_FACTOR); + let new_rate_smoothed = new_rate_min.max(new_rate_max.min(new_rate)); + + // Prevent reward rate from dropping below 1 or exceeding BUS_EPOCH_REWARDS and return. + new_rate_smoothed.max(1).min(BUS_EPOCH_REWARDS) +} + +/// Creates a new pda +#[inline(always)] +pub fn create_pda<'a, 'info>( + target_account: &'a AccountInfo<'info>, + owner: &Pubkey, + space: usize, + pda_seeds: &[&[u8]], + system_program: &'a AccountInfo<'info>, + payer: &'a AccountInfo<'info>, +) -> ProgramResult { + let rent = Rent::get()?; + solana_program::program::invoke_signed( + &solana_program::system_instruction::create_account( + payer.key, + target_account.key, + rent.minimum_balance(space as usize), + space as u64, + owner, + ), + &[ + payer.clone(), + target_account.clone(), + system_program.clone(), + ], + &[pda_seeds], + )?; + Ok(()) +} + +/// The seed of the bus account PDA. +pub const BUS: &[u8] = b"bus"; + +/// The seed of the mint account PDA. +pub const MINT: &[u8] = b"mint"; + +/// The seed of the proof account PDA. +pub const PROOF: &[u8] = b"proof"; + +/// The seed of the treasury account PDA. +pub const TREASURY: &[u8] = b"treasury"; + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +pub struct Bus { + /// The bump of the bus account PDA. + pub bump: u32, + + /// The ID of the bus account. + pub id: u32, + + /// The quantity of rewards this bus can issue in the current epoch epoch. + pub available_rewards: u64, +} + +impl Bus { + pub fn to_bytes(&self) -> &[u8] { + bytemuck::bytes_of(self) + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +pub struct Treasury { + /// The bump of the treasury account PDA. + pub bump: u64, + + /// The admin authority with permission to update the difficulty. + pub admin: Pubkey, + + /// The hash difficulty. + // pub difficulty: Hash, + + /// The timestamp of the start of the current epoch. + pub epoch_start_at: i64, + + /// The reward rate to payout to miners for submiting valid hashes. + pub reward_rate: u64, + + /// The total lifetime claimed rewards. + pub total_claimed_rewards: u64, +} + +impl Treasury { + pub fn to_bytes(&self) -> &[u8] { + bytemuck::bytes_of(self) + } +} + +#[cfg(test)] +mod tests { + use solana_program::{ + keccak::{hashv, Hash}, + pubkey::Pubkey, + }; + + use crate::{calculate_new_reward_rate, validate_hash, SMOOTHING_FACTOR, TARGET_EPOCH_REWARDS}; + + #[test] + fn test_validate_hash_pass() { + let h1 = Hash::new_from_array([1; 32]); + let signer = Pubkey::new_unique(); + let nonce = 10u64; + let difficulty = Hash::new_from_array([255; 32]); + let h2 = hashv(&[ + h1.to_bytes().as_slice(), + signer.to_bytes().as_slice(), + nonce.to_be_bytes().as_slice(), + ]); + let res = validate_hash(h1, h2, signer, nonce, difficulty); + assert!(res.is_ok()); + } + + #[test] + fn test_validate_hash_fail() { + let h1 = Hash::new_from_array([1; 32]); + let signer = Pubkey::new_unique(); + let nonce = 10u64; + let difficulty = Hash::new_from_array([255; 32]); + let h2 = Hash::new_from_array([2; 32]); + let res = validate_hash(h1, h2, signer, nonce, difficulty); + assert!(res.is_err()); + } + + #[test] + fn test_validate_hash_fail_difficulty() { + let h1 = Hash::new_from_array([1; 32]); + let signer = Pubkey::new_unique(); + let nonce = 10u64; + let difficulty = Hash::new_from_array([0; 32]); + let h2 = hashv(&[ + h1.to_bytes().as_slice(), + signer.to_bytes().as_slice(), + nonce.to_be_bytes().as_slice(), + ]); + let res = validate_hash(h1, h2, signer, nonce, difficulty); + assert!(res.is_err()); + } + + #[test] + fn test_calculate_new_reward_rate_stable() { + let current_rate = 1000; + let new_rate = calculate_new_reward_rate(current_rate, TARGET_EPOCH_REWARDS); + assert!(new_rate.eq(¤t_rate)); + } + + #[test] + fn test_calculate_new_reward_rate_no_chage() { + let current_rate = 1000; + let new_rate = calculate_new_reward_rate(current_rate, 0); + assert!(new_rate.eq(¤t_rate)); + } + + #[test] + fn test_calculate_new_reward_rate_lower() { + let current_rate = 1000; + let new_rate = + calculate_new_reward_rate(current_rate, TARGET_EPOCH_REWARDS.saturating_add(1_000_000)); + assert!(new_rate.lt(¤t_rate)); + } + + #[test] + fn test_calculate_new_reward_rate_higher() { + let current_rate = 1000; + let new_rate = + calculate_new_reward_rate(current_rate, TARGET_EPOCH_REWARDS.saturating_sub(1_000_000)); + assert!(new_rate.gt(¤t_rate)); + } + + #[test] + fn test_calculate_new_reward_rate_max_smooth() { + let current_rate = 1000; + let new_rate = calculate_new_reward_rate(current_rate, 1); + assert!(new_rate.eq(¤t_rate.saturating_mul(SMOOTHING_FACTOR))); + } + + #[test] + fn test_calculate_new_reward_rate_min_smooth() { + let current_rate = 1000; + let new_rate = calculate_new_reward_rate(current_rate, u64::MAX); + assert!(new_rate.eq(¤t_rate.saturating_div(SMOOTHING_FACTOR))); + } +} diff --git a/src/loaders.rs b/src/loaders.rs new file mode 100644 index 0000000..9ed1d87 --- /dev/null +++ b/src/loaders.rs @@ -0,0 +1,65 @@ +use solana_program::{ + account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, system_program, +}; + +pub fn load_signer<'a, 'info>( + info: &'a AccountInfo<'info>, +) -> Result<&'a AccountInfo<'info>, ProgramError> { + if !info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + Ok(info) +} + +pub fn load_pda<'a, 'info>( + info: &'a AccountInfo<'info>, + seeds: &[&[u8]], + writable: bool, +) -> Result<&'a AccountInfo<'info>, ProgramError> { + let key = Pubkey::create_program_address(seeds, &crate::id())?; + if !info.key.eq(&key) { + return Err(ProgramError::InvalidSeeds); + } + if !info.owner.eq(&crate::id()) { + return Err(ProgramError::InvalidAccountOwner); + } + if writable { + if !info.is_writable { + return Err(ProgramError::InvalidAccountData); + } + } + Ok(info) +} + +pub fn load_uninitialized_pda<'a, 'info>( + info: &'a AccountInfo<'info>, + seeds: &[&[u8]], +) -> Result<&'a AccountInfo<'info>, ProgramError> { + let key = Pubkey::create_program_address(seeds, &crate::id())?; + if !info.key.eq(&key) { + return Err(ProgramError::InvalidSeeds); + } + load_uninitialized_account(info) +} + +pub fn load_uninitialized_account<'a, 'info>( + info: &'a AccountInfo<'info>, +) -> Result<&'a AccountInfo<'info>, ProgramError> { + if !info.is_writable { + return Err(ProgramError::InvalidAccountData); + } + if !info.data_is_empty() || !info.owner.eq(&system_program::id()) { + return Err(ProgramError::AccountAlreadyInitialized); + } + Ok(info) +} + +pub fn load_account<'a, 'info>( + info: &'a AccountInfo<'info>, + key: Pubkey, +) -> Result<&'a AccountInfo<'info>, ProgramError> { + if !info.key.eq(&key) { + return Err(ProgramError::InvalidAccountData); + } + Ok(info) +} diff --git a/tests/test_initialize.rs b/tests/test_initialize.rs new file mode 100644 index 0000000..6a81305 --- /dev/null +++ b/tests/test_initialize.rs @@ -0,0 +1,121 @@ +use ore::{ + instruction::{InitializeArgs, OreInstruction}, + Bus, Treasury, BUS, BUS_COUNT, INITIAL_REWARD_RATE, MINT, TREASURY, +}; +use solana_program::{ + hash::Hash, + instruction::{AccountMeta, Instruction}, + program_option::COption, + program_pack::Pack, + pubkey::Pubkey, + system_program, sysvar, +}; +use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_sdk::{ + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use spl_token::state::Mint; + +#[tokio::test] +async fn test_initialize() { + // 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(system_program::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(sysvar::rent::id(), false), + ], + data: [ + OreInstruction::Initialize.to_vec(), + InitializeArgs { + bus_0_bump: bus_pdas[0].1, + bus_1_bump: bus_pdas[1].1, + bus_2_bump: bus_pdas[2].1, + bus_3_bump: bus_pdas[3].1, + bus_4_bump: bus_pdas[4].1, + bus_5_bump: bus_pdas[5].1, + bus_6_bump: bus_pdas[6].1, + bus_7_bump: bus_pdas[7].1, + mint_bump: mint_pda.1, + treasury_bump: treasury_pda.1, + } + .to_bytes() + .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(); + assert_eq!(bus.bump as u8, bus_pdas[i].1); + assert_eq!(bus.id as u8, i as u8); + assert_eq!(bus.available_rewards, 0); + } + + // 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, payer.pubkey()); + assert_eq!(treasury.epoch_start_at as u8, 0); + assert_eq!(treasury.reward_rate, INITIAL_REWARD_RATE); + assert_eq!(treasury.total_claimed_rewards as u8, 0); + + // 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, 0); + assert_eq!(mint.decimals, ore::TOKEN_DECIMALS); + assert_eq!(mint.is_initialized, true); + assert_eq!(mint.freeze_authority, COption::None); +} + +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); + program_test.start().await +}