diff --git a/Cargo.lock b/Cargo.lock index ac56cd9..cc54743 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1751,7 +1751,6 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus", - "cw2", "cw20", "cw20-base", "hex", @@ -1763,7 +1762,6 @@ dependencies = [ "serde-json-wasm", "serde_json", "serde_repr", - "thiserror 2.0.16", ] [[package]] diff --git a/artifacts/checksums.txt b/artifacts/checksums.txt index 277788a..2bc072d 100644 --- a/artifacts/checksums.txt +++ b/artifacts/checksums.txt @@ -1,2 +1,2 @@ -40bc067d21fb1a80e05b57d8f95cb10296dc1db322dddb56daf68d6f53ad64c3 dex_aggregator.wasm +6751147df3820468674b76e49dd069642513f36b59404847de1ca5e9ce4eca7c dex_aggregator.wasm 46dcce8409830a30b424dd342887f1cc203ef9c203f4b754b172c2203a7c0ad7 mock_swap.wasm diff --git a/artifacts/dex_aggregator.wasm b/artifacts/dex_aggregator.wasm index b331c03..30a8ec1 100644 Binary files a/artifacts/dex_aggregator.wasm and b/artifacts/dex_aggregator.wasm differ diff --git a/contracts/dex_aggregator/src/execute.rs b/contracts/dex_aggregator/src/execute.rs index 4f96095..86097ca 100644 --- a/contracts/dex_aggregator/src/execute.rs +++ b/contracts/dex_aggregator/src/execute.rs @@ -132,6 +132,27 @@ pub fn create_swap_cosmos_msg( } } Operation::OrderbookSwap(ob_op) => { + let tick_size_atomic = ob_op.min_quantity_tick_size; + + if tick_size_atomic.is_zero() { + return Err(ContractError::Std(StdError::generic_err( + "min_quantity_tick_size cannot be zero", + ))); + } + + let ratio = amount / tick_size_atomic; + let rounded_atomic_amount = ratio * tick_size_atomic; + + if rounded_atomic_amount.is_zero() { + return Ok(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_json_binary(&{})?, + funds: vec![], + })); + } + + let quantity_for_query_fp = FPDecimal::from(rounded_atomic_amount); + let offer_denom = match &ob_op.offer_asset_info { amm::AssetInfo::NativeToken { denom } => denom.clone(), @@ -150,7 +171,7 @@ pub fn create_swap_cosmos_msg( }; let simulate_msg = msg::orderbook::QueryMsg::GetOutputQuantity { - from_quantity: amount.into(), + from_quantity: quantity_for_query_fp, source_denom: offer_denom, target_denom: target_denom.clone(), }; @@ -159,10 +180,13 @@ pub fn create_swap_cosmos_msg( .query_wasm_smart(&ob_op.swap_contract, &simulate_msg)?; let expected_output_fp = simulation_response.result_quantity; let slippage = FPDecimal::from_str("0.005")?; - let min_output_fp = expected_output_fp * (FPDecimal::ONE - slippage); + + let min_output_with_slippage_fp = expected_output_fp * (FPDecimal::ONE - slippage); + let floored_min_output_fp = min_output_with_slippage_fp.int(); + let swap_msg = orderbook::OrderbookExecuteMsg::SwapMinOutput { target_denom, - min_output_quantity: min_output_fp, + min_output_quantity: floored_min_output_fp, }; let funds = vec![Coin { @@ -170,7 +194,7 @@ pub fn create_swap_cosmos_msg( amm::AssetInfo::NativeToken { denom } => denom.clone(), _ => unreachable!(), }, - amount, + amount: rounded_atomic_amount, }]; CosmosMsg::Wasm(WasmMsg::Execute { diff --git a/contracts/dex_aggregator/src/msg.rs b/contracts/dex_aggregator/src/msg.rs index b4c7d5f..6de5d9e 100644 --- a/contracts/dex_aggregator/src/msg.rs +++ b/contracts/dex_aggregator/src/msg.rs @@ -131,6 +131,7 @@ pub struct OrderbookSwapOp { pub swap_contract: String, pub offer_asset_info: amm::AssetInfo, pub ask_asset_info: amm::AssetInfo, + pub min_quantity_tick_size: Uint128, } #[cw_serde] diff --git a/contracts/dex_aggregator/src/reply.rs b/contracts/dex_aggregator/src/reply.rs index 4947809..e8838ef 100644 --- a/contracts/dex_aggregator/src/reply.rs +++ b/contracts/dex_aggregator/src/reply.rs @@ -460,28 +460,34 @@ fn parse_amount_from_swap_reply(msg: &Reply) -> Result { .map_err(|e| ContractError::SubmessageResultError { error: e })? .events; - let amount_str_opt = events // Changed to an Option - .iter() - .find_map(|event| { - if !event.ty.starts_with("wasm") { - return None; - } - let key = if event.ty == "wasm-atomic_swap_execution" { - "swap_final_amount" + let amount_str_opt = events.iter().find_map(|event| { + if !event.ty.starts_with("wasm") { + return None; + } + let key = if event.ty == "wasm-atomic_swap_execution" { + "swap_final_amount" + } else { + "return_amount" + }; + event + .attributes + .iter() + .find(|attr| attr.key == key) + .map(|attr| attr.value.clone()) + }); + + match amount_str_opt { + Some(amount_str) => { + let integer_part_str = if let Some(period_pos) = amount_str.find('.') { + &amount_str[..period_pos] } else { - "return_amount" + &amount_str }; - event - .attributes - .iter() - .find(|attr| attr.key == key) - .map(|attr| attr.value.clone()) - }); - match amount_str_opt { - Some(amount_str) => amount_str - .parse::() - .map_err(|_| ContractError::MalformedAmountInReply { value: amount_str }), + integer_part_str + .parse::() + .map_err(|_| ContractError::MalformedAmountInReply { value: amount_str }) + } None => Ok(Uint128::zero()), } } diff --git a/contracts/mock_swap/Cargo.toml b/contracts/mock_swap/Cargo.toml index 68e432d..9734253 100644 --- a/contracts/mock_swap/Cargo.toml +++ b/contracts/mock_swap/Cargo.toml @@ -10,7 +10,6 @@ crate-type = ["cdylib", "rlib"] library = [] [dependencies] -cw2 = { version = "2.0.0" } cw20 = { version = "2.0.0" } cosmwasm-schema = { version = "2.2.2" } cosmwasm-std = { version = "2.2.2", features = [ @@ -32,10 +31,7 @@ serde = { version = "1.0.219", default-features = false, features = serde-json-wasm = { version = "1.0.0" } serde_json = { version = "1.0.140" } serde_repr = { version = "0.1.20" } - -thiserror = { version = "2.0.12" } - -hex = { version = "0.4.3" } +hex = { version = "0.4.3" } [dev-dependencies] injective-test-tube = { version = "1.16.3-1" } diff --git a/tests/integration.rs b/tests/integration.rs index 5b84d2d..5a0c240 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -321,6 +321,7 @@ fn test_aggregate_swap_success() { offer_asset_info: amm::AssetInfo::NativeToken { denom: "inj".to_string(), }, + min_quantity_tick_size: Uint128::new(1_000_000_000_000_000), })], }, ], @@ -407,6 +408,7 @@ fn test_multi_stage_aggregate_swap_success() { offer_asset_info: amm::AssetInfo::NativeToken { denom: "usdt".to_string(), }, + min_quantity_tick_size: Uint128::new(10000), })], }], }, @@ -985,6 +987,7 @@ fn test_full_normalization_route() { ask_asset_info: amm::AssetInfo::NativeToken { denom: native_shroom_denom.clone(), }, + min_quantity_tick_size: Uint128::new(1_000_000_000_000_000), })], }, Split { @@ -1070,6 +1073,7 @@ fn test_multi_stage_with_final_normalization() { ask_asset_info: amm::AssetInfo::NativeToken { denom: "inj".to_string(), }, + min_quantity_tick_size: Uint128::new(10000), })], }], }, @@ -1098,6 +1102,7 @@ fn test_multi_stage_with_final_normalization() { ask_asset_info: amm::AssetInfo::NativeToken { denom: native_shroom_denom.clone(), }, + min_quantity_tick_size: Uint128::new(1_000_000_000_000_000), })], }, ], @@ -1278,6 +1283,7 @@ fn test_reverse_normalization_route() { ask_asset_info: amm::AssetInfo::NativeToken { denom: "usdt".to_string(), }, + min_quantity_tick_size: Uint128::new(10000), })], }], }, @@ -1377,6 +1383,7 @@ fn test_failure_if_minimum_receive_not_met() { offer_asset_info: amm::AssetInfo::NativeToken { denom: "inj".to_string(), }, + min_quantity_tick_size: Uint128::new(1_000_000_000_000_000), })], }, ], @@ -1553,6 +1560,7 @@ fn test_mixed_input_unified_output_reconciliation() { swap_contract: setup.mock_native_shroom_to_usdt_ob.clone(), offer_asset_info: native_shroom_info.clone(), ask_asset_info: usdt_info.clone(), + min_quantity_tick_size: Uint128::new(10000), })], }, Split { @@ -1661,6 +1669,7 @@ fn test_cw20_input_with_initial_reconciliation() { swap_contract: setup.mock_native_shroom_to_usdt_ob.clone(), offer_asset_info: native_shroom_info.clone(), ask_asset_info: usdt_info.clone(), + min_quantity_tick_size: Uint128::new(10000), })], }, Split { @@ -1765,6 +1774,7 @@ fn test_complex_reconciliation_mixed_to_mixed() { swap_contract: setup.mock_inj_to_native_shroom_ob.clone(), offer_asset_info: inj_info.clone(), ask_asset_info: native_shroom_info.clone(), + min_quantity_tick_size: Uint128::new(1_000_000_000_000_000), })], }, Split { @@ -1789,6 +1799,7 @@ fn test_complex_reconciliation_mixed_to_mixed() { swap_contract: setup.mock_native_shroom_to_usdt_ob.clone(), offer_asset_info: native_shroom_info.clone(), ask_asset_info: usdt_info.clone(), + min_quantity_tick_size: Uint128::new(10000), })], }, Split { @@ -2161,6 +2172,7 @@ fn test_stage_with_single_hundred_percent_split() { offer_asset_info: amm::AssetInfo::NativeToken { denom: "usdt".to_string(), }, + min_quantity_tick_size: Uint128::new(10000), })], }], }; @@ -2239,6 +2251,7 @@ fn test_intermediate_swap_failure_reverts_transaction() { offer_asset_info: amm::AssetInfo::NativeToken { denom: "usdt".to_string(), }, + min_quantity_tick_size: Uint128::new(10000), })], }], }; @@ -3076,12 +3089,14 @@ fn test_multi_hop_path_with_mid_path_conversion() { swap_contract: setup.mock_native_shroom_to_usdt_ob.clone(), offer_asset_info: native_shroom_info.clone(), ask_asset_info: usdt_info.clone(), + min_quantity_tick_size: Uint128::new(10000), }), // Hop 3: USDT -> INJ Operation::OrderbookSwap(OrderbookSwapOp { swap_contract: setup.mock_usdt_to_inj_ob.clone(), offer_asset_info: usdt_info.clone(), ask_asset_info: inj_info.clone(), + min_quantity_tick_size: Uint128::new(10000), }), ];