diff --git a/ixo-swap/src/contract.rs b/ixo-swap/src/contract.rs index 2135d07..e37d195 100644 --- a/ixo-swap/src/contract.rs +++ b/ixo-swap/src/contract.rs @@ -27,7 +27,7 @@ use crate::state::{ }; use crate::token_amount::TokenAmount; use crate::utils::{ - decimal_to_uint128, MIN_FEE_PERCENT, PREDEFINED_MAX_FEES_PERCENT, PREDEFINED_MAX_SLIPPAGE_PERCENT, SCALE_FACTOR + calculate_amount_with_percent, decimal_to_uint128, MIN_FEE_PERCENT, PREDEFINED_MAX_FEES_PERCENT, PREDEFINED_MAX_SLIPPAGE_PERCENT, SCALE_FACTOR }; // Version info for migration info @@ -580,12 +580,12 @@ fn validate_slippage( min_token_amount: Uint128, actual_token_amount: Uint128, ) -> Result<(), ContractError> { - let max_slippage_percent = MAX_SLIPPAGE_PERCENT.load(deps.storage)?; + let max_slippage = MAX_SLIPPAGE_PERCENT.load(deps.storage)?; + let max_slippage_percent = decimal_to_uint128(max_slippage)?; + + let slippage_impact = calculate_amount_with_percent(actual_token_amount, max_slippage_percent)?; - let actual_token_decimal_amount = Decimal::from_str(actual_token_amount.to_string().as_str())?; - let slippage_impact = actual_token_decimal_amount * max_slippage_percent; - let min_required_decimal_amount = actual_token_decimal_amount - slippage_impact; - let min_required_amount = min_required_decimal_amount.to_uint_floor(); + let min_required_amount = actual_token_amount - slippage_impact; if min_token_amount < min_required_amount { return Err(ContractError::MinTokenAmountError { @@ -2179,7 +2179,7 @@ mod tests { let mut deps = mock_dependencies(); MAX_SLIPPAGE_PERCENT - .save(&mut deps.storage, &Decimal::from_str("0.1").unwrap()) + .save(&mut deps.storage, &Decimal::from_str("2").unwrap()) .unwrap(); let min_token_amount = Uint128::new(95_000); @@ -2191,7 +2191,7 @@ mod tests { err, ContractError::MinTokenAmountError { min_token: min_token_amount, - min_required: Uint128::new(99_000) + min_required: Uint128::new(107_800) // 110_000 * 0.98 (2% slippage) = 107_800 } ); } @@ -2204,7 +2204,7 @@ mod tests { .save(&mut deps.storage, &Decimal::from_str("0.1").unwrap()) .unwrap(); - let min_token_amount = Uint128::new(108_000); + let min_token_amount = Uint128::new(109_900); // 110_000 * 0.999 (0.1% slippage) = 109_890 let actual_token_amount = Uint128::new(110_000); let res = validate_slippage(&deps.as_mut(), min_token_amount, actual_token_amount).unwrap(); diff --git a/ixo-swap/src/integration_test.rs b/ixo-swap/src/integration_test.rs index 94f0069..3dd5616 100644 --- a/ixo-swap/src/integration_test.rs +++ b/ixo-swap/src/integration_test.rs @@ -775,7 +775,7 @@ fn cw1155_to_cw20_swap() { let token_ids = vec![TokenId::from("FIRST/1"), TokenId::from("FIRST/2")]; - let max_slippage_percent = Decimal::from_str("0.3").unwrap(); + let max_slippage_percent = Decimal::from_str("8").unwrap(); let lp_fee_percent = Decimal::from_str("0.2").unwrap(); let protocol_fee_percent = Decimal::from_str("0.1").unwrap(); @@ -899,7 +899,6 @@ fn cw1155_to_cw20_swap() { let res = router .execute_contract(owner.clone(), amm.clone(), &swap_msg, &[]) .unwrap(); - println!("res: {:?}", res); let event = Event::new("wasm").add_attributes(vec![ attr("action", "swap"), attr("sender", owner.clone()), @@ -938,8 +937,8 @@ fn cw1155_to_cw20_swap() { input_token: TokenSelect::Token2, input_amount: TokenAmount::Single(Uint128::new(60_000)), min_output: TokenAmount::Multiple(HashMap::from([ - (token_ids[0].clone(), Uint128::new(30_000)), - (token_ids[1].clone(), Uint128::new(30_000)), + (token_ids[0].clone(), Uint128::new(33_000)), + (token_ids[1].clone(), Uint128::new(33_000)), ])), expiration: None, }; @@ -950,7 +949,7 @@ fn cw1155_to_cw20_swap() { // ensure balances updated let owner_balance = batch_balance_for_owner(&router, &cw1155_token, &owner, &token_ids).balances; - assert_eq!(owner_balance, [Uint128::new(65_878), Uint128::new(55_000)]); + assert_eq!(owner_balance, [Uint128::new(62_878), Uint128::new(58_000)]); let owner_balance = cw20_token.balance(&router.wrap(), owner.clone()).unwrap(); assert_eq!(owner_balance, Uint128::new(23_266)); let fee_recipient_balance = cw20_token @@ -984,7 +983,7 @@ fn cw1155_to_native_swap() { let cw1155_token = create_cw1155(&mut router, &owner); let token_ids = vec![TokenId::from("FIRST/1"), TokenId::from("FIRST/2")]; - let max_slippage_percent = Decimal::from_str("0.3").unwrap(); + let max_slippage_percent = Decimal::from_str("8").unwrap(); let lp_fee_percent = Decimal::from_str("0.2").unwrap(); let protocol_fee_percent = Decimal::from_str("0.1").unwrap(); @@ -1087,8 +1086,8 @@ fn cw1155_to_native_swap() { input_token: TokenSelect::Token2, input_amount: TokenAmount::Single(Uint128::new(60_000)), min_output: TokenAmount::Multiple(HashMap::from([ - (token_ids[0].clone(), Uint128::new(30_000)), - (token_ids[1].clone(), Uint128::new(30_000)), + (token_ids[0].clone(), Uint128::new(34_000)), + (token_ids[1].clone(), Uint128::new(34_000)), ])), expiration: None, }; @@ -1107,7 +1106,7 @@ fn cw1155_to_native_swap() { // ensure balances updated let owner_balance = batch_balance_for_owner(&router, &cw1155_token, &owner, &token_ids).balances; - assert_eq!(owner_balance, [Uint128::new(65_878), Uint128::new(55_000)]); + assert_eq!(owner_balance, [Uint128::new(61_878), Uint128::new(59_000)]); let owner_balance: Coin = bank_balance(&mut router, &owner, NATIVE_TOKEN_DENOM.to_string()); assert_eq!(owner_balance.amount, Uint128::new(23_266)); let fee_recipient_balance = bank_balance( @@ -1143,7 +1142,7 @@ fn cw1155_to_native_swap_low_fees() { let cw1155_token = create_cw1155(&mut router, &owner); let token_ids = vec![TokenId::from("FIRST/1"), TokenId::from("FIRST/2")]; - let max_slippage_percent = Decimal::from_str("0.3").unwrap(); + let max_slippage_percent = Decimal::from_str("8").unwrap(); let lp_fee_percent = Decimal::from_str("0.0").unwrap(); let protocol_fee_percent = Decimal::from_str("0.01").unwrap(); @@ -1223,7 +1222,7 @@ fn cw1155_to_native_swap_low_fees() { (token_ids[0].clone(), Uint128::new(5_000)), (token_ids[1].clone(), Uint128::new(5_000)), ])), - min_output: TokenAmount::Single(Uint128::new(6_500)), + min_output: TokenAmount::Single(Uint128::new(8_362)), expiration: None, }; let _res = router @@ -1247,8 +1246,8 @@ fn cw1155_to_native_swap_low_fees() { input_token: TokenSelect::Token2, input_amount: TokenAmount::Single(Uint128::new(7_000)), min_output: TokenAmount::Multiple(HashMap::from([ - (token_ids[0].clone(), Uint128::new(3_000)), - (token_ids[1].clone(), Uint128::new(3_000)), + (token_ids[0].clone(), Uint128::new(3_700)), + (token_ids[1].clone(), Uint128::new(3_700)), ])), expiration: None, }; @@ -1267,7 +1266,7 @@ fn cw1155_to_native_swap_low_fees() { // ensure balances updated let owner_balance = batch_balance_for_owner(&router, &cw1155_token, &owner, &token_ids).balances; - assert_eq!(owner_balance, [Uint128::new(49_863), Uint128::new(48_000)]); + assert_eq!(owner_balance, [Uint128::new(49_163), Uint128::new(48_700)]); let owner_balance: Coin = bank_balance(&mut router, &owner, NATIVE_TOKEN_DENOM.to_string()); assert_eq!(owner_balance.amount, Uint128::new(52_090)); let fee_recipient_balance = bank_balance( @@ -1336,7 +1335,7 @@ fn amm_add_and_remove_liquidity() { let cw1155_token = create_cw1155(&mut router, &owner); - let max_slippage_percent = Decimal::from_str("0.3").unwrap(); + let max_slippage_percent = Decimal::from_str("1").unwrap(); let supported_denom = "CARBON".to_string(); let token_ids = vec![ @@ -1718,7 +1717,7 @@ fn amm_add_and_remove_liquidity() { let remove_liquidity_msg = ExecuteMsg::RemoveLiquidity { amount: Uint128::new(50), min_token1155: TokenAmount::Multiple(HashMap::from([ - (token_ids[0].clone(), Uint128::new(35)), + (token_ids[0].clone(), Uint128::new(45)), (token_ids[1].clone(), Uint128::new(5)), ])), min_token2: Uint128::new(50), @@ -1741,12 +1740,12 @@ fn amm_add_and_remove_liquidity() { // ensure balances updated let owner_balance = batch_balance_for_owner(&router, &cw1155_token, &owner, &token_ids).balances; - assert_eq!(owner_balance, [Uint128::new(4915), Uint128::new(4985)]); + assert_eq!(owner_balance, [Uint128::new(4925), Uint128::new(4975)]); let token_supplies = get_owner_lp_tokens_balance(&router, &amm_addr, &token_ids).supplies; - assert_eq!(token_supplies, [Uint128::new(85), Uint128::new(15)]); + assert_eq!(token_supplies, [Uint128::new(75), Uint128::new(25)]); let amm_balances = batch_balance_for_owner(&router, &cw1155_token, &amm_addr, &token_ids).balances; - assert_eq!(amm_balances, [Uint128::new(85), Uint128::new(15)]); + assert_eq!(amm_balances, [Uint128::new(75), Uint128::new(25)]); let crust_balance = lp_token.balance(&router.wrap(), owner.clone()).unwrap(); assert_eq!(crust_balance, Uint128::new(100)); @@ -1763,8 +1762,8 @@ fn amm_add_and_remove_liquidity() { let remove_liquidity_msg = ExecuteMsg::RemoveLiquidity { amount: Uint128::new(100), min_token1155: TokenAmount::Multiple(HashMap::from([ - (token_ids[0].clone(), Uint128::new(85)), - (token_ids[1].clone(), Uint128::new(15)), + (token_ids[0].clone(), Uint128::new(75)), + (token_ids[1].clone(), Uint128::new(25)), ])), min_token2: Uint128::new(100), expiration: None, @@ -1803,7 +1802,7 @@ fn remove_liquidity_with_partially_and_any_filling() { let cw1155_token = create_cw1155(&mut router, &owner); - let max_slippage_percent = Decimal::from_str("0.3").unwrap(); + let max_slippage_percent = Decimal::from_str("5").unwrap(); let supported_denom = "CARBON".to_string(); let token_ids = vec![ @@ -1938,8 +1937,9 @@ fn remove_liquidity_with_partially_and_any_filling() { let remove_liquidity_msg = ExecuteMsg::RemoveLiquidity { amount: Uint128::new(80), min_token1155: TokenAmount::Multiple(HashMap::from([ - (token_ids[0].clone(), Uint128::new(40)), + (token_ids[0].clone(), Uint128::new(41)), (token_ids[1].clone(), Uint128::new(30)), + (token_ids[2].clone(), Uint128::new(5)), ])), min_token2: Uint128::new(80), expiration: None, @@ -1996,8 +1996,8 @@ fn remove_liquidity_with_partially_and_any_filling() { let remove_liquidity_msg = ExecuteMsg::RemoveLiquidity { amount: Uint128::new(55), - min_token1155: TokenAmount::Single(Uint128::new(40)), - min_token2: Uint128::new(40), + min_token1155: TokenAmount::Single(Uint128::new(52)), + min_token2: Uint128::new(52), expiration: None, }; let _res = router diff --git a/ixo-swap/src/token_amount.rs b/ixo-swap/src/token_amount.rs index 42d103c..28241f8 100644 --- a/ixo-swap/src/token_amount.rs +++ b/ixo-swap/src/token_amount.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, convert::TryFrom}; +use std::collections::HashMap; use cosmwasm_schema::cw_serde; use cosmwasm_std::{CheckedMultiplyFractionError, Decimal, Uint128}; @@ -6,7 +6,7 @@ use cw1155::TokenId; use crate::{ error::ContractError, - utils::{decimal_to_uint128, SCALE_FACTOR}, + utils::{calculate_amount_with_percent, decimal_to_uint128}, }; #[cw_serde] @@ -133,16 +133,8 @@ impl TokenAmount { input_amount: Uint128, percent: Uint128, ) -> Result { - if percent.is_zero() || input_amount.is_zero() { - return Ok(TokenAmount::Single(Uint128::zero())); - } - - let fraction = (SCALE_FACTOR.u128(), 1u128); - let result = input_amount - .full_mul(percent) - .checked_div_ceil(fraction) - .map_err(|err| err)?; - Ok(TokenAmount::Single(Uint128::try_from(result)?)) + let result = calculate_amount_with_percent(input_amount, percent)?; + Ok(TokenAmount::Single(result)) } } @@ -175,6 +167,17 @@ mod tests { assert_eq!(fee.get_total(), Uint128::new(1)) } + #[test] + fn should_return_zero_when_input_is_zero() { + let token_amount = TokenAmount::Single(Uint128::new(0)); + let fee = token_amount + .get_percent(Decimal::from_str("0.1").unwrap()) + .unwrap() + .unwrap(); + + assert_eq!(fee.get_total(), Uint128::new(0)) + } + #[test] fn should_return_fee_amount_when_multiple_input_token_provided_and_two_token_amount_are_over() { let token_amount = TokenAmount::Multiple(HashMap::from([ diff --git a/ixo-swap/src/utils.rs b/ixo-swap/src/utils.rs index aa5e961..13ac2c4 100644 --- a/ixo-swap/src/utils.rs +++ b/ixo-swap/src/utils.rs @@ -1,4 +1,6 @@ -use cosmwasm_std::{Decimal, StdError, StdResult, Uint128}; +use std::convert::TryFrom; + +use cosmwasm_std::{CheckedMultiplyFractionError, Decimal, StdError, StdResult, Uint128}; /// The minimum fee percent allowed is 0.01%, based of the SCALE_FACTOR, /// otherwise it will always end up with 0 fee if lower than 0.01% @@ -8,6 +10,7 @@ pub const PREDEFINED_MAX_FEES_PERCENT: &str = "5"; pub const PREDEFINED_MAX_SLIPPAGE_PERCENT: &str = "10"; pub const DECIMAL_PRECISION: Uint128 = Uint128::new(10u128.pow(20)); +/// Converts a Decimal to a Uint128 with the SCALE_FACTOR applied, so that Uint128::1 is 0.01% pub fn decimal_to_uint128(decimal: Decimal) -> StdResult { let result: Uint128 = decimal .atomics() @@ -16,3 +19,20 @@ pub fn decimal_to_uint128(decimal: Decimal) -> StdResult { Ok(result / DECIMAL_PRECISION) } + +// Utility function to calculate amount based on percent +pub fn calculate_amount_with_percent( + input_amount: Uint128, + percent: Uint128, +) -> Result { + if percent.is_zero() || input_amount.is_zero() { + return Ok(Uint128::zero()); + } + + let fraction = (SCALE_FACTOR.u128(), 1u128); + let result = input_amount + .full_mul(percent) + .checked_div_ceil(fraction) + .map_err(|err| err)?; + Ok(Uint128::try_from(result)?) +} \ No newline at end of file