diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml index 3643188c..e65dae38 100644 --- a/.github/workflows/rust.yaml +++ b/.github/workflows/rust.yaml @@ -9,12 +9,12 @@ on: workflow_dispatch: env: + RUSTVERS: "1.90.0" # only applies to non solana platform tools rustc + SOLANAVERS: "2.2.20" # all the stuff that usually gets cached to ~ # now gets cached to repo dir so that cache action can pick it up HOME: ${{ github.workspace }} CARGO_HOME: ${{ github.workspace }}/.cargo - RUSTVERS: "1.90.0" # only applies to non solana platform tools rustc - SOLANAVERS: "2.2.20" jobs: # hax from https://github.com/orgs/community/discussions/26324#discussioncomment-3251460 @@ -56,7 +56,7 @@ jobs: path: | ${{ env.CARGO_HOME }} target - key: ${{ runner.os }}-rustlint-${{ hashFiles('**/Cargo.lock') }} + key: ${{ runner.os }}-rustlint-${{ env.RUSTVERS }}-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-rustlint- - name: Clippy @@ -98,7 +98,7 @@ jobs: path: | ${{ env.CARGO_HOME }} target - key: ${{ runner.os }}-sbf-${{ hashFiles('**/Cargo.lock') }} + key: ${{ runner.os }}-sbf-${{ env.SOLANAVERS }}-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-sbf- - name: Build diff --git a/Cargo.toml b/Cargo.toml index 61f16b89..75546900 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ version = "0.1.0" # In general, keep default-features = false in workspace # and enable individual features in indiv crates anyhow = { version = "^1", default-features = false } +# TODO: migrate to five8 bs58-fixed = { git = "https://github.com/igneous-labs/bs58-fixed.git", branch = "master", default-features = false } bs58-fixed-wasm = { git = "https://github.com/igneous-labs/bs58-fixed.git", branch = "master", default-features = false } const-crypto = { version = "^0.3.0", default-features = false } @@ -46,8 +47,8 @@ generic-array-struct = { version = "^0.3.1", default-features = false } hmac-sha256 = { version = "^1", default-features = false } jupiter-amm-interface = { version = "^0.6", default-features = false } rust_decimal = { version = "^1.36.0", default-features = false } # vers constraint based on jupiter-amm-interface -sanctum-fee-ratio = { version = "^2", default-features = false } -sanctum-u64-ratio = { version = "^2", default-features = false } +sanctum-fee-ratio = { version = "^2.1", default-features = false } +sanctum-u64-ratio = { version = "^2.1", default-features = false } serde = { version = "^1", default-features = false } serde_bytes = { version = "^0.11", default-features = false } tsify-next = { version = "^0.5.5", default-features = false } @@ -63,6 +64,9 @@ rand = { version = "^0.9", default-features = false } serde_json = { version = "^1", default-features = false } # solana crates (for dev) +# Make sure lowest vers number of solana-* subdeps matches +# solana toolchain vers in README bec solana-verify +# seems to select toolchain vers based on that mollusk-svm = { version = "^0.5", default-features = false } mollusk-svm-programs-token = { version = "^0.5", default-features = false } solana-account = { version = "^2.2.1", default-features = false } # vers constraint based on solana-sdk subdep diff --git a/README.md b/README.md index 49be84fe..a863d910 100644 --- a/README.md +++ b/README.md @@ -16,3 +16,9 @@ solana-cargo-build-sbf 2.2.20 platform-tools v1.48 rustc 1.84.1 ``` + +Install with + +```sh +sh -c "$(curl -sSfL https://release.anza.xyz/v2.2.20/install)" +``` diff --git a/jup-interface/Cargo.toml b/jup-interface/Cargo.toml index 26a89a50..a063fd47 100644 --- a/jup-interface/Cargo.toml +++ b/jup-interface/Cargo.toml @@ -5,7 +5,7 @@ license-file.workspace = true version.workspace = true include = ["src/**/*", "Cargo.toml"] -[dependencies] +[target.'cfg(not(target_os = "solana"))'.dependencies] anyhow = { workspace = true } inf1-std = { workspace = true } jupiter-amm-interface = { workspace = true } @@ -18,7 +18,7 @@ solana-instruction = { workspace = true } solana-pubkey = { workspace = true } solana-sha256-hasher = { workspace = true } -[dev-dependencies] +[target.'cfg(not(target_os = "solana"))'.dev-dependencies] generic-array-struct = { workspace = true } inf1-test-utils = { workspace = true } lazy_static = { workspace = true } diff --git a/jup-interface/src/lib.rs b/jup-interface/src/lib.rs index e4bd9277..5cb33a7b 100644 --- a/jup-interface/src/lib.rs +++ b/jup-interface/src/lib.rs @@ -1,3 +1,6 @@ +#![allow(unexpected_cfgs)] +#![cfg(not(target_os = "solana"))] + use std::{ iter::once, sync::{ diff --git a/pricing/flatslab/core/src/pricing.rs b/pricing/flatslab/core/src/pricing.rs index 6d3f27d3..430cdf48 100644 --- a/pricing/flatslab/core/src/pricing.rs +++ b/pricing/flatslab/core/src/pricing.rs @@ -29,6 +29,16 @@ pub struct FlatSlabSwapPricing { pub out_fee_nanos: FeeNanos, } +/// Checks +impl FlatSlabSwapPricing { + #[inline] + pub const fn is_net_negative(&self) -> bool { + // unchecked-arith: FeeNanos valid range will not overflow + self.inp_fee_nanos.get() + self.out_fee_nanos.get() < 0 + } +} + +/// Pricing impl FlatSlabSwapPricing { /// Returns the ratio that returns out_sol_value /// when applied to in_sol_value @@ -47,7 +57,8 @@ impl FlatSlabSwapPricing { // post_fee_nanos = 1_000_000_000 - fee_nanos // out_sol_value = floor(in_sol_value * post_fee_nanos / 1_000_000_000) // i32 signed subtraction: - // - rebates are allowed (post_fee_nanos > 1_000_000_000) + // - rebates are allowed here (post_fee_nanos > 1_000_000_000) + // but should've been filtered out by `self.is_net_negative()` check // - however, >100% fees will error (post_fee_nanos < 0) let post_fee_nanos = match NANOS_DENOM.checked_sub(fee_nanos) { None => return None, @@ -65,31 +76,42 @@ impl FlatSlabSwapPricing { } #[inline] - pub const fn pp_price_exact_in(&self, in_sol_value: u64) -> Option { - match self.out_ratio() { - None => None, - Some(r) => r.apply(in_sol_value), + pub const fn pp_price_exact_in(&self, in_sol_value: u64) -> Result { + if self.is_net_negative() { + return Err(FlatSlabPricingErr::NetNegativeFees); + } + let r = match self.out_ratio() { + None => return Err(FlatSlabPricingErr::Ratio), + Some(r) => r, + }; + match r.apply(in_sol_value) { + None => Err(FlatSlabPricingErr::Ratio), + Some(x) => Ok(x), } } #[inline] - pub const fn pp_price_exact_out(&self, out_sol_value: u64) -> Option { + pub const fn pp_price_exact_out(&self, out_sol_value: u64) -> Result { + if self.is_net_negative() { + return Err(FlatSlabPricingErr::NetNegativeFees); + } // the greatest possible non-u64::MAX value of in_sol_value is 1_000_000_00 x out_sol_value. // Otherwise if fee is 100% then this will return None unless out_sol_value == 0 - let range_opt = match self.out_ratio() { - None => return None, - Some(r) => r.reverse(out_sol_value), + let r = match self.out_ratio() { + None => return Err(FlatSlabPricingErr::Ratio), + Some(r) => r, }; - let range = match range_opt { - None => return None, + let range = match r.reverse(out_sol_value) { + None => return Err(FlatSlabPricingErr::Ratio), Some(r) => r, }; - Some(*range.end()) + Ok(*range.end()) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum FlatSlabPricingErr { + NetNegativeFees, Ratio, } @@ -97,6 +119,7 @@ impl Display for FlatSlabPricingErr { #[inline] fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_str(match self { + Self::NetNegativeFees => "net negative fees disallowed", Self::Ratio => "ratio math error", }) } @@ -113,7 +136,6 @@ impl PriceExactIn for FlatSlabSwapPricing { PriceExactInIxArgs { sol_value, .. }: PriceExactInIxArgs, ) -> Result { self.pp_price_exact_in(sol_value) - .ok_or(FlatSlabPricingErr::Ratio) } } @@ -126,7 +148,6 @@ impl PriceExactOut for FlatSlabSwapPricing { PriceExactOutIxArgs { sol_value, .. }: PriceExactOutIxArgs, ) -> Result { self.pp_price_exact_out(sol_value) - .ok_or(FlatSlabPricingErr::Ratio) } } @@ -140,7 +161,6 @@ impl PriceLpTokensToRedeem for FlatSlabSwapPricing { PriceLpTokensToRedeemIxArgs { sol_value, .. }: PriceLpTokensToRedeemIxArgs, ) -> Result { self.pp_price_exact_in(sol_value) - .ok_or(FlatSlabPricingErr::Ratio) } } @@ -153,7 +173,6 @@ impl PriceLpTokensToMint for FlatSlabSwapPricing { PriceLpTokensToMintIxArgs { sol_value, .. }: PriceLpTokensToMintIxArgs, ) -> Result { self.pp_price_exact_in(sol_value) - .ok_or(FlatSlabPricingErr::Ratio) } } @@ -293,6 +312,19 @@ mod tests { } } + prop_compose! { + /// inp out nanos pair that will result in a fee rate < 0 + fn net_neg_fee() + (i in *FeeNanos::MIN..=-*FeeNanos::MIN) // since abs(MIN) < abs(MAX) + (i in Just(i), o in *FeeNanos::MIN..-i) + -> FlatSlabSwapPricing { + FlatSlabSwapPricing { + inp_fee_nanos: FeeNanos::new(i).unwrap(), + out_fee_nanos: FeeNanos::new(o).unwrap() + } + } + } + // General proptest! { @@ -546,4 +578,27 @@ mod tests { prop_assert_eq!(mint_sol_value, sol_value); } } + + proptest! { + #[allow(deprecated)] + #[test] + fn net_neg_fee_errs( + fee in net_neg_fee(), + sol_value: u64, + amt: u64, // dont-care + ) { + let args = PriceExactInIxArgs { sol_value, amt }; + for f in [ + FlatSlabSwapPricing::price_exact_in, + FlatSlabSwapPricing::price_exact_out, + FlatSlabSwapPricing::price_lp_tokens_to_redeem, + FlatSlabSwapPricing::price_lp_tokens_to_mint, + ] { + prop_assert_eq!( + f(&fee, args), + Err(FlatSlabPricingErr::NetNegativeFees), + ); + } + } + } } diff --git a/pricing/flatslab/program/src/err.rs b/pricing/flatslab/program/src/err.rs index ddae7c96..bcb28489 100644 --- a/pricing/flatslab/program/src/err.rs +++ b/pricing/flatslab/program/src/err.rs @@ -1,6 +1,6 @@ //! TODO: this should maybe be in its own `inf1-pp-flatslab-jiminy` crate -use inf1_pp_flatslab_core::errs::FlatSlabProgramErr; +use inf1_pp_flatslab_core::{errs::FlatSlabProgramErr, pricing::FlatSlabPricingErr}; use jiminy_entrypoint::program_error::ProgramError; /// Example-usage: @@ -64,7 +64,8 @@ seqerr!( FeeNanosOutOfRange(_), MintNotFound(_), MissingAdminSignature, - Pricing(_), + Pricing(FlatSlabPricingErr::NetNegativeFees), + Pricing(FlatSlabPricingErr::Ratio), WrongSlabAcc ); diff --git a/pricing/flatslab/program/src/instructions/pricing/exact_in.rs b/pricing/flatslab/program/src/instructions/pricing/exact_in.rs index e4dd4948..8b79cdde 100644 --- a/pricing/flatslab/program/src/instructions/pricing/exact_in.rs +++ b/pricing/flatslab/program/src/instructions/pricing/exact_in.rs @@ -24,7 +24,8 @@ pub fn process_price_exact_in( }, )? .price_exact_in(args) - .map_err(|e| CustomProgErr(FlatSlabProgramErr::Pricing(e)))?; + .map_err(FlatSlabProgramErr::Pricing) + .map_err(CustomProgErr)?; set_return_data(&ret.to_le_bytes()); Ok(()) } diff --git a/pricing/flatslab/program/src/instructions/pricing/exact_out.rs b/pricing/flatslab/program/src/instructions/pricing/exact_out.rs index 8f93f61b..9f168116 100644 --- a/pricing/flatslab/program/src/instructions/pricing/exact_out.rs +++ b/pricing/flatslab/program/src/instructions/pricing/exact_out.rs @@ -24,7 +24,8 @@ pub fn process_price_exact_out( }, )? .price_exact_out(args) - .map_err(|e| CustomProgErr(FlatSlabProgramErr::Pricing(e)))?; + .map_err(FlatSlabProgramErr::Pricing) + .map_err(CustomProgErr)?; set_return_data(&ret.to_le_bytes()); Ok(()) } diff --git a/pricing/flatslab/program/src/instructions/pricing/mint_lp.rs b/pricing/flatslab/program/src/instructions/pricing/mint_lp.rs index 808af496..faf5f8fd 100644 --- a/pricing/flatslab/program/src/instructions/pricing/mint_lp.rs +++ b/pricing/flatslab/program/src/instructions/pricing/mint_lp.rs @@ -20,9 +20,9 @@ pub fn process_price_lp_tokens_to_mint( let ret = slab .entries() .pricing(&pair) - .map_err(|e| CustomProgErr(FlatSlabProgramErr::MintNotFound(e)))? - .price_exact_in(args) - .map_err(|e| CustomProgErr(FlatSlabProgramErr::Pricing(e)))?; + .map_err(FlatSlabProgramErr::MintNotFound) + .and_then(|p| p.price_exact_in(args).map_err(FlatSlabProgramErr::Pricing)) + .map_err(CustomProgErr)?; set_return_data(&ret.to_le_bytes()); Ok(()) } diff --git a/pricing/flatslab/program/src/instructions/pricing/redeem_lp.rs b/pricing/flatslab/program/src/instructions/pricing/redeem_lp.rs index e351f4a2..a3e19f66 100644 --- a/pricing/flatslab/program/src/instructions/pricing/redeem_lp.rs +++ b/pricing/flatslab/program/src/instructions/pricing/redeem_lp.rs @@ -20,9 +20,9 @@ pub fn process_price_lp_tokens_to_redeem( let ret = slab .entries() .pricing(&pair) - .map_err(|e| CustomProgErr(FlatSlabProgramErr::MintNotFound(e)))? - .price_exact_in(args) - .map_err(|e| CustomProgErr(FlatSlabProgramErr::Pricing(e)))?; + .map_err(FlatSlabProgramErr::MintNotFound) + .and_then(|p| p.price_exact_in(args).map_err(FlatSlabProgramErr::Pricing)) + .map_err(CustomProgErr)?; set_return_data(&ret.to_le_bytes()); Ok(()) } diff --git a/pricing/flatslab/program/tests/common/props.rs b/pricing/flatslab/program/tests/common/props.rs index dfaf6f6d..35160fce 100644 --- a/pricing/flatslab/program/tests/common/props.rs +++ b/pricing/flatslab/program/tests/common/props.rs @@ -1,3 +1,8 @@ +//! FIXME: refactor tests: +//! - move generation to test-utils crate +//! - change generation from "account to args" style to +//! "args to account" style, like everything else in test-utils gen/ folder + use std::ops::RangeInclusive; use inf1_pp_core::pair::Pair; diff --git a/pricing/flatslab/program/tests/tests/pricing/exact_in.rs b/pricing/flatslab/program/tests/tests/pricing/exact_in.rs index 61ffa0d6..cb115c8d 100644 --- a/pricing/flatslab/program/tests/tests/pricing/exact_in.rs +++ b/pricing/flatslab/program/tests/tests/pricing/exact_in.rs @@ -37,16 +37,23 @@ proptest! { &accs.seq().cloned().collect::>(), ); let lib_res = pricing.price_exact_in(args); + match (program_result, lib_res) { (ProgramResult::Success, Ok(lib_res)) => { prop_assert_eq!(lib_res, u64::from_le_bytes(return_data.try_into().unwrap())); } - (ProgramResult::Failure(e), Err(_)) => { + (ProgramResult::Failure(e), Err(FlatSlabPricingErr::Ratio)) => { assert_prog_err_eq( &e, &ProgramError::from(CustomProgErr(FlatSlabProgramErr::Pricing(FlatSlabPricingErr::Ratio))) ); }, + (ProgramResult::Failure(e), Err(FlatSlabPricingErr::NetNegativeFees)) => { + assert_prog_err_eq( + &e, + &ProgramError::from(CustomProgErr(FlatSlabProgramErr::Pricing(FlatSlabPricingErr::NetNegativeFees))) + ); + }, (a, b) => { panic!("{a:#?}, {b:#?}"); } diff --git a/pricing/flatslab/program/tests/tests/pricing/exact_out.rs b/pricing/flatslab/program/tests/tests/pricing/exact_out.rs index 75ef48db..afb0037c 100644 --- a/pricing/flatslab/program/tests/tests/pricing/exact_out.rs +++ b/pricing/flatslab/program/tests/tests/pricing/exact_out.rs @@ -62,16 +62,23 @@ proptest! { &accs.seq().cloned().collect::>(), ); let lib_res = pricing.price_exact_out(args); + match (program_result, lib_res) { (ProgramResult::Success, Ok(lib_res)) => { prop_assert_eq!(lib_res, u64::from_le_bytes(return_data.try_into().unwrap())); } - (ProgramResult::Failure(e), Err(_)) => { + (ProgramResult::Failure(e), Err(FlatSlabPricingErr::Ratio)) => { assert_prog_err_eq( &e, &ProgramError::from(CustomProgErr(FlatSlabProgramErr::Pricing(FlatSlabPricingErr::Ratio))) ); }, + (ProgramResult::Failure(e), Err(FlatSlabPricingErr::NetNegativeFees)) => { + assert_prog_err_eq( + &e, + &ProgramError::from(CustomProgErr(FlatSlabProgramErr::Pricing(FlatSlabPricingErr::NetNegativeFees))) + ); + }, (a, b) => { panic!("{a:#?}, {b:#?}"); } diff --git a/test-utils/Cargo.toml b/test-utils/Cargo.toml index f5604c91..ea3569e2 100644 --- a/test-utils/Cargo.toml +++ b/test-utils/Cargo.toml @@ -5,7 +5,7 @@ license-file.workspace = true version.workspace = true publish = false -[dependencies] +[target.'cfg(not(target_os = "solana"))'.dependencies] generic-array-struct = { workspace = true } glob = { workspace = true } inf1-ctl-core = { workspace = true } diff --git a/test-utils/src/lib.rs b/test-utils/src/lib.rs index e4dce0fc..a2b34815 100644 --- a/test-utils/src/lib.rs +++ b/test-utils/src/lib.rs @@ -1,3 +1,6 @@ +#![allow(unexpected_cfgs)] +#![cfg(not(target_os = "solana"))] + mod accounts; mod diff; mod fixtures; diff --git a/ts/sdk/src/err.rs b/ts/sdk/src/err.rs index c4ce3d82..0398f6d8 100644 --- a/ts/sdk/src/err.rs +++ b/ts/sdk/src/err.rs @@ -374,7 +374,9 @@ impl From for InfError { const ERR_PREFIX: &str = "FlatSlabPricingErr::"; let (code, cause) = match e { - FlatSlabPricingErr::Ratio => (InfErr::InternalErr, format!("{ERR_PREFIX}{e}")), + FlatSlabPricingErr::Ratio | FlatSlabPricingErr::NetNegativeFees => { + (InfErr::InternalErr, format!("{ERR_PREFIX}{e}")) + } }; InfError { code,