diff --git a/jup-sdk/Cargo.lock b/jup-sdk/Cargo.lock index d65c26cb1..d93913657 100644 --- a/jup-sdk/Cargo.lock +++ b/jup-sdk/Cargo.lock @@ -1451,7 +1451,7 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futarchy-amm-jup-sdk" -version = "0.1.2" +version = "0.1.3" dependencies = [ "ahash 0.8.12", "anchor-lang", diff --git a/jup-sdk/Cargo.toml b/jup-sdk/Cargo.toml index d0925ebd0..7966e70ad 100644 --- a/jup-sdk/Cargo.toml +++ b/jup-sdk/Cargo.toml @@ -2,7 +2,7 @@ name = "futarchy-amm-jup-sdk" description = "SDK for interacting with Futarchy AMM, built for Jupiter" license = "BUSL-1.1" -version = "0.1.2" +version = "0.1.3" edition = "2024" [dependencies] @@ -16,5 +16,3 @@ ahash = "0.8.12" solana-client = "2.2.1" solana-commitment-config = "2.2.1" solana-sdk = "2.3.1" - - diff --git a/jup-sdk/src/futarchy_amm.rs b/jup-sdk/src/futarchy_amm.rs index 5361ea5fd..450726f7a 100644 --- a/jup-sdk/src/futarchy_amm.rs +++ b/jup-sdk/src/futarchy_amm.rs @@ -1,9 +1,12 @@ use anchor_lang::prelude::{ - borsh, AccountMeta, AnchorDeserialize, AnchorSerialize, InitSpace, Pubkey, + AccountMeta, AnchorDeserialize, AnchorSerialize, InitSpace, Pubkey, borsh, }; -use anyhow::{anyhow, bail, Result}; +use anyhow::{Result, anyhow, bail}; -use crate::FutarchyAmmError; +use crate::{ + FutarchyAmmError, + lamport::{Lamport, ToLamport}, +}; // use crate::{FutarchyError, LP_TAKER_FEE_BPS, MAX_BPS, PROTOCOL_TAKER_FEE_BPS}; pub const LP_TAKER_FEE_BPS: u16 = 25; @@ -185,12 +188,9 @@ impl Pool { } pub fn swap(&mut self, input_amount: u64, swap_type: SwapType) -> Result { - let input_amount_after_protocol_fee = (input_amount as u128) - .checked_mul((MAX_BPS - PROTOCOL_TAKER_FEE_BPS) as u128) - .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))? - .checked_div(MAX_BPS as u128) - .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))? - as u64; + let input_amount_after_protocol_fee = (input_amount.lamports() + * (MAX_BPS - PROTOCOL_TAKER_FEE_BPS).lamports() + / MAX_BPS.lamports())?; let k = self.k(); @@ -198,53 +198,34 @@ impl Pool { SwapType::Buy => (self.quote_reserves, self.base_reserves), SwapType::Sell => (self.base_reserves, self.quote_reserves), }; + let (input_reserve, output_reserve) = (input_reserve.lamports(), output_reserve.lamports()); // airlifted from uniswap v1: // https://github.com/Uniswap/v1-contracts/blob/c10c08d81d6114f694baa8bd32f555a40f6264da/contracts/uniswap_exchange.vy#L106-L111 - if input_reserve == 0 || output_reserve == 0 { + if input_reserve == Lamport::ZERO || output_reserve == Lamport::ZERO { bail!(FutarchyAmmError::InvalidReserves); } - let input_amount_after_lp_fee = (input_amount_after_protocol_fee as u128) - .checked_mul((MAX_BPS - LP_TAKER_FEE_BPS) as u128) - .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))?; + let input_amount_after_lp_fee = + (input_amount_after_protocol_fee * (MAX_BPS - LP_TAKER_FEE_BPS).lamports())?; - let numerator = input_amount_after_lp_fee - .checked_mul(output_reserve as u128) - .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))?; + let numerator = (input_amount_after_lp_fee * output_reserve)?; - let denominator = (input_reserve as u128) - .checked_mul(MAX_BPS as u128) - .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))? - .checked_add(input_amount_after_lp_fee as u128) - .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))?; + let denominator = (input_reserve * MAX_BPS.lamports() + input_amount_after_lp_fee)?; - let output_amount = (numerator - .checked_div(denominator) - .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))?) - as u64; + let output_amount = (numerator / denominator)?; match swap_type { SwapType::Buy => { - self.quote_reserves = self - .quote_reserves - .checked_add(input_amount_after_protocol_fee) - .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))?; - self.base_reserves = self - .base_reserves - .checked_sub(output_amount) - .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))?; + self.quote_reserves = + (self.quote_reserves.lamports() + input_amount_after_protocol_fee)?.val(); + self.base_reserves = (self.base_reserves.lamports() - output_amount)?.val(); } SwapType::Sell => { - self.base_reserves = self - .base_reserves - .checked_add(input_amount_after_protocol_fee) - .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))?; - self.quote_reserves = self - .quote_reserves - .checked_sub(output_amount) - .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))?; + self.base_reserves = + (self.base_reserves.lamports() + input_amount_after_protocol_fee)?.val(); + self.quote_reserves = (self.quote_reserves.lamports() - output_amount)?.val(); } } @@ -254,57 +235,40 @@ impl Pool { bail!(FutarchyAmmError::AmmInvariantViolated); } - Ok(output_amount) + Ok(output_amount.val()) } pub fn feeless_swap(&mut self, input_amount: u64, swap_type: SwapType) -> Result { let k = self.k(); + let input_amount = input_amount.lamports(); let (input_reserve, output_reserve) = match swap_type { SwapType::Buy => (self.quote_reserves, self.base_reserves), SwapType::Sell => (self.base_reserves, self.quote_reserves), }; + let (input_reserve, output_reserve) = (input_reserve.lamports(), output_reserve.lamports()); // airlifted from uniswap v1: // https://github.com/Uniswap/v1-contracts/blob/c10c08d81d6114f694baa8bd32f555a40f6264da/contracts/uniswap_exchange.vy#L106-L111 - if input_reserve == 0 || output_reserve == 0 { + if input_reserve == Lamport::ZERO || output_reserve == Lamport::ZERO { bail!(FutarchyAmmError::InvalidReserves); } - let numerator = (input_amount as u128) - .checked_mul(output_reserve as u128) - .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))?; + let numerator = (input_amount * output_reserve)?; - let denominator = (input_reserve as u128) - .checked_add(input_amount as u128) - .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))?; + let denominator = (input_reserve + input_amount)?; - let output_amount = (numerator - .checked_div(denominator) - .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))?) - as u64; + let output_amount = (numerator / denominator)?; match swap_type { SwapType::Buy => { - self.quote_reserves = self - .quote_reserves - .checked_add(input_amount) - .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))?; - self.base_reserves = self - .base_reserves - .checked_sub(output_amount) - .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))?; + self.quote_reserves = (self.quote_reserves.lamports() + input_amount)?.val(); + self.base_reserves = (self.base_reserves.lamports() - output_amount)?.val(); } SwapType::Sell => { - self.base_reserves = self - .base_reserves - .checked_add(input_amount) - .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))?; - self.quote_reserves = self - .quote_reserves - .checked_sub(output_amount) - .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))?; + self.base_reserves = (self.base_reserves.lamports() + input_amount)?.val(); + self.quote_reserves = (self.quote_reserves.lamports() - output_amount)?.val(); } } @@ -314,7 +278,7 @@ impl Pool { bail!(FutarchyAmmError::AmmInvariantViolated); } - Ok(output_amount) + Ok(output_amount.val()) } pub fn simulate_swap(&self, input_amount: u64, swap_type: SwapType) -> Result { diff --git a/jup-sdk/src/lamport.rs b/jup-sdk/src/lamport.rs new file mode 100644 index 000000000..fda44cba3 --- /dev/null +++ b/jup-sdk/src/lamport.rs @@ -0,0 +1,114 @@ +use crate::FutarchyAmmError; +use anyhow::anyhow; +use std::ops::{Add, Div, Mul, Sub}; + +/// A wrapper for lamport values that will bail on +/// any overflows. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct Lamport(u128); + +impl Lamport { + pub const ZERO: Self = Lamport(0); + + pub const fn new(lamports: u64) -> Self { + Lamport(lamports as _) + } + + pub const fn val(self) -> u64 { + self.0 as _ + } +} + +impl Add for Lamport { + type Output = anyhow::Result; + fn add(self, rhs: Self) -> Self::Output { + self.0 + .checked_add(rhs.0) + .map(|v| Self(v)) + .ok_or(anyhow!(FutarchyAmmError::MathOverflow)) + } +} + +impl Sub for Lamport { + type Output = anyhow::Result; + fn sub(self, rhs: Self) -> Self::Output { + self.0 + .checked_sub(rhs.0) + .map(|v| Self(v)) + .ok_or(anyhow!(FutarchyAmmError::MathOverflow)) + } +} + +impl Div for Lamport { + type Output = anyhow::Result; + fn div(self, rhs: Self) -> Self::Output { + self.0 + .checked_div(rhs.0) + .map(|v| Self(v)) + .ok_or(anyhow!(FutarchyAmmError::MathOverflow)) + } +} + +impl Mul for Lamport { + type Output = anyhow::Result; + fn mul(self, rhs: Self) -> Self::Output { + self.0 + .checked_mul(rhs.0) + .map(|v| Self(v)) + .ok_or(anyhow!(FutarchyAmmError::MathOverflow)) + } +} + +impl Div> for Lamport { + type Output = anyhow::Result; + fn div(self, rhs: anyhow::Result) -> Self::Output { + rhs.and_then(|rhs| { + self.0 + .checked_div(rhs.0) + .map(|v| Self(v)) + .ok_or(anyhow!(FutarchyAmmError::MathOverflow)) + }) + } +} + +impl Mul> for Lamport { + type Output = anyhow::Result; + fn mul(self, rhs: anyhow::Result) -> Self::Output { + rhs.and_then(|rhs| { + self.0 + .checked_mul(rhs.0) + .map(|v| Self(v)) + .ok_or(anyhow!(FutarchyAmmError::MathOverflow)) + }) + } +} + +impl Div for anyhow::Result { + type Output = Self; + fn div(self, rhs: Lamport) -> Self::Output { + self.and_then(|v| v / rhs) + } +} + +impl Add for anyhow::Result { + type Output = Self; + fn add(self, rhs: Lamport) -> Self::Output { + self.and_then(|v| v + rhs) + } +} + +pub trait ToLamport { + fn lamports(self) -> Lamport; +} + +impl ToLamport for u64 { + fn lamports(self) -> Lamport { + Lamport::new(self) + } +} + +impl ToLamport for u16 { + fn lamports(self) -> Lamport { + Lamport::new(self as _) + } +} diff --git a/jup-sdk/src/lib.rs b/jup-sdk/src/lib.rs index 8b34ee2ec..fdba00c08 100644 --- a/jup-sdk/src/lib.rs +++ b/jup-sdk/src/lib.rs @@ -1,18 +1,20 @@ -use anchor_lang::prelude::{AnchorDeserialize, Pubkey, Space}; +pub mod futarchy_amm; +pub mod lamport; + +pub use futarchy_amm::{FutarchyAmm, MAX_BPS, TAKER_FEE_BPS}; -use anyhow::{Context, Result, anyhow, bail}; +use crate::{ + futarchy_amm::{FutarchyAmmSwap, SwapType}, + lamport::ToLamport, +}; +use anchor_lang::prelude::{AnchorDeserialize, Pubkey, Space}; +use anyhow::{Context, Result, bail}; use jupiter_amm_interface::{ AccountMap, Amm, AmmContext, AmmProgramIdToLabel, KeyedAccount, Quote, Swap, SwapAndAccountMetas, SwapMode, SwapParams, }; - -pub mod futarchy_amm; - -pub use futarchy_amm::{FutarchyAmm, MAX_BPS, TAKER_FEE_BPS}; use rust_decimal::Decimal; -use crate::futarchy_amm::{FutarchyAmmSwap, SwapType}; - pub const FUTARCHY_PROGRAM_ID: Pubkey = Pubkey::from_str_const("FUTARELBfJfQ8RDGhg1wdhddq1odMAJUePHFuBYfUxKq"); pub const SPL_TOKEN_PROGRAM_ID: Pubkey = @@ -41,6 +43,8 @@ impl std::fmt::Display for FutarchyAmmError { } } +impl std::error::Error for FutarchyAmmError {} + #[derive(Debug, Clone)] pub struct FutarchyAmmClient { pub dao_address: Pubkey, @@ -176,12 +180,8 @@ impl Amm for FutarchyAmmClient { let fee_pct = Decimal::new(TAKER_FEE_BPS as i64, 2); // this isn't exact because of compounding, but should be close enough - let fee_amount = (quote_params.amount as u128) - .checked_mul(TAKER_FEE_BPS as u128) - .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))? - .checked_div(MAX_BPS as u128) - .ok_or_else(|| anyhow!(FutarchyAmmError::MathOverflow))? - as u64; + let fee_amount = + (quote_params.amount.lamports() * TAKER_FEE_BPS.lamports() / MAX_BPS.lamports())?.val(); Ok(Quote { in_amount: quote_params.amount,