diff --git a/contracts/swap/msg.rs b/contracts/swap/msg.rs new file mode 100644 index 0000000..f0c841e --- /dev/null +++ b/contracts/swap/msg.rs @@ -0,0 +1,70 @@ +use cosmwasm_std::{Addr, Coin}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use injective_cosmwasm::MarketId; +use injective_math::FPDecimal; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum FeeRecipient { + Address(Addr), + SwapContract, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct InstantiateMsg { + pub fee_recipient: FeeRecipient, + pub admin: Addr, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ExecuteMsg { + SwapMinOutput { + target_denom: String, + min_output_quantity: FPDecimal, + }, + SwapExactOutput { + target_denom: String, + target_output_quantity: FPDecimal, + }, + SetRoute { + source_denom: String, + target_denom: String, + route: Vec, + }, + DeleteRoute { + source_denom: String, + target_denom: String, + }, + UpdateConfig { + admin: Option, + fee_recipient: Option, + }, + WithdrawSupportFunds { + coins: Vec, + target_address: Addr, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + GetRoute { + source_denom: String, + target_denom: String, + }, + GetOutputQuantity { + from_quantity: FPDecimal, + source_denom: String, + target_denom: String, + }, + GetInputQuantity { + to_quantity: FPDecimal, + source_denom: String, + target_denom: String, + }, + GetAllRoutes {}, + GetConfig {}, +} diff --git a/contracts/swap/src/testing/integration_tests/integration_logic_tests.rs b/contracts/swap/src/testing/integration_tests/integration_logic_tests.rs new file mode 100644 index 0000000..d009000 --- /dev/null +++ b/contracts/swap/src/testing/integration_tests/integration_logic_tests.rs @@ -0,0 +1,2233 @@ +use cosmwasm_std::{coin, Addr}; + +use injective_test_tube::RunnerError::{ExecuteError, QueryError}; +use injective_test_tube::{ + Account, Bank, Exchange, InjectiveTestApp, Module, RunnerError, RunnerResult, SigningAccount, + Wasm, +}; + +use injective_math::{round_to_min_tick, FPDecimal}; + +use crate::msg::{ExecuteMsg, QueryMsg}; +use crate::testing::test_utils::{ + are_fpdecimals_approximately_equal, assert_fee_is_as_expected, create_limit_order, + fund_account_with_some_inj, human_to_dec, init_contract_with_fee_recipient_and_get_address, + init_default_signer_account, init_default_validator_account, init_rich_account, + init_self_relaying_contract_and_get_address, launch_spot_market, must_init_account_with_funds, + pause_spot_market, query_all_bank_balances, query_bank_balance, set_route_and_assert_success, + str_coin, Decimals, OrderSide, ATOM, DEFAULT_ATOMIC_MULTIPLIER, DEFAULT_RELAYER_SHARE, + DEFAULT_SELF_RELAYING_FEE_PART, DEFAULT_TAKER_FEE, ETH, INJ, USDC, USDT, +}; +use crate::types::{FPCoin, SwapEstimationResult}; + +/* + This suite of tests focuses on calculation logic itself and doesn't attempt to use neither + realistic market configuration nor order prices, so that we don't have to deal with scaling issues. + + Hardcoded values used in these tests come from the first tab of this spreadsheet: + https://docs.google.com/spreadsheets/d/1-0epjX580nDO_P2mm1tSjhvjJVppsvrO1BC4_wsBeyA/edit?usp=sharing +*/ + +#[test] +fn it_executes_a_swap_between_two_base_assets_with_multiple_price_levels() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = init_default_signer_account(&app); + let _validator = init_default_validator_account(&app); + let owner = init_rich_account(&app); + + let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); + let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("100_000", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + create_eth_buy_orders(&app, &spot_market_1_id, &trader1, &trader2); + create_atom_sell_orders(&app, &spot_market_2_id, &trader1, &trader2, &trader3); + + app.increase_time(1); + + let swapper = must_init_account_with_funds( + &app, + &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], + ); + + let mut query_result: SwapEstimationResult = wasm + .query( + &contr_addr, + &QueryMsg::GetOutputQuantity { + source_denom: ETH.to_string(), + target_denom: ATOM.to_string(), + from_quantity: FPDecimal::from(12u128), + }, + ) + .unwrap(); + + assert_eq!( + query_result.result_quantity, + FPDecimal::must_from_str("2893.886"), //slightly rounded down + "incorrect swap result estimate returned by query" + ); + + assert_eq!( + query_result.expected_fees.len(), + 2, + "Wrong number of fee denoms received" + ); + + let mut expected_fees = vec![ + FPCoin { + amount: FPDecimal::must_from_str("3541.5"), + denom: "usdt".to_string(), + }, + FPCoin { + amount: FPDecimal::must_from_str("3530.891412"), + denom: "usdt".to_string(), + }, + ]; + + assert_fee_is_as_expected( + &mut query_result.expected_fees, + &mut expected_fees, + FPDecimal::must_from_str("0.000001"), + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + + wasm.execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: ATOM.to_string(), + min_output_quantity: FPDecimal::from(2800u128), + }, + &[coin(12, ETH)], + &swapper, + ) + .unwrap(); + + let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + assert_eq!( + from_balance, + FPDecimal::ZERO, + "some of the original amount wasn't swapped" + ); + assert_eq!( + to_balance, + FPDecimal::must_from_str("2893"), + "swapper did not receive expected amount" + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let contract_balance_usdt_after = + FPDecimal::must_from_str(contract_balances_after[0].amount.as_str()); + let contract_balance_usdt_before = + FPDecimal::must_from_str(contract_balances_before[0].amount.as_str()); + + assert!( + contract_balance_usdt_after >= contract_balance_usdt_before, + "Contract lost some money after swap. Balance before: {contract_balance_usdt_before}, after: {contract_balance_usdt_after}", + ); + + let max_diff = human_to_dec("0.00001", Decimals::Six); + + assert!( + are_fpdecimals_approximately_equal( + contract_balance_usdt_after, + contract_balance_usdt_before, + max_diff, + ), + "Contract balance changed too much. Before: {}, After: {}", + contract_balances_before[0].amount, + contract_balances_after[0].amount + ); +} + +#[test] +fn it_executes_a_swap_between_two_base_assets_with_single_price_level() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = init_default_signer_account(&app); + let _validator = init_default_validator_account(&app); + let owner = init_rich_account(&app); + + let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); + let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("100_000", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + create_eth_buy_orders(&app, &spot_market_1_id, &trader1, &trader2); + create_atom_sell_orders(&app, &spot_market_2_id, &trader1, &trader2, &trader3); + + app.increase_time(1); + + let swapper = must_init_account_with_funds( + &app, + &[coin(3, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], + ); + + let expected_atom_estimate_quantity = FPDecimal::must_from_str("751.492"); + let mut query_result: SwapEstimationResult = wasm + .query( + &contr_addr, + &QueryMsg::GetOutputQuantity { + source_denom: ETH.to_string(), + target_denom: ATOM.to_string(), + from_quantity: FPDecimal::from(3u128), + }, + ) + .unwrap(); + + assert_eq!( + query_result.result_quantity, expected_atom_estimate_quantity, + "incorrect swap result estimate returned by query" + ); + + let mut expected_fees = vec![ + FPCoin { + amount: FPDecimal::must_from_str("904.5"), + denom: "usdt".to_string(), + }, + FPCoin { + amount: FPDecimal::must_from_str("901.790564"), + denom: "usdt".to_string(), + }, + ]; + + assert_fee_is_as_expected( + &mut query_result.expected_fees, + &mut expected_fees, + human_to_dec("0.00001", Decimals::Six), + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + wasm.execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: ATOM.to_string(), + min_output_quantity: FPDecimal::from(750u128), + }, + &[coin(3, ETH)], + &swapper, + ) + .unwrap(); + + let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + assert_eq!( + from_balance, + FPDecimal::ZERO, + "some of the original amount wasn't swapped" + ); + assert_eq!( + to_balance, + expected_atom_estimate_quantity.int(), + "swapper did not receive expected amount" + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + assert_eq!( + contract_balances_after, contract_balances_before, + "contract balance has changed after swap" + ); +} + +#[test] +fn it_executes_swap_between_markets_using_different_quote_assets() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = init_default_signer_account(&app); + let _validator = init_default_validator_account(&app); + let owner = init_rich_account(&app); + + let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); + let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDC); + let spot_market_3_id = launch_spot_market(&exchange, &owner, USDC, USDT); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[ + str_coin("100_000", USDC, Decimals::Six), + str_coin("100_000", USDT, Decimals::Six), + ], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_3_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + create_eth_buy_orders(&app, &spot_market_1_id, &trader1, &trader2); + create_atom_sell_orders(&app, &spot_market_2_id, &trader1, &trader2, &trader3); + + //USDT-USDC + create_limit_order( + &app, + &trader3, + &spot_market_3_id, + OrderSide::Sell, + 1, + 100_000_000, + ); + + app.increase_time(1); + + let swapper = must_init_account_with_funds( + &app, + &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], + ); + + let mut query_result: SwapEstimationResult = wasm + .query( + &contr_addr, + &QueryMsg::GetOutputQuantity { + source_denom: ETH.to_string(), + target_denom: ATOM.to_string(), + from_quantity: FPDecimal::from(12u128), + }, + ) + .unwrap(); + + // expected amount is a bit lower, even though 1 USDT = 1 USDC, because of the fees + assert_eq!( + query_result.result_quantity, + FPDecimal::must_from_str("2889.64"), + "incorrect swap result estimate returned by query" + ); + + let mut expected_fees = vec![ + FPCoin { + amount: FPDecimal::must_from_str("3541.5"), + denom: "usdt".to_string(), + }, + FPCoin { + amount: FPDecimal::must_from_str("3530.891412"), + denom: "usdt".to_string(), + }, + FPCoin { + amount: FPDecimal::must_from_str("3525.603007"), + denom: "usdc".to_string(), + }, + ]; + + assert_fee_is_as_expected( + &mut query_result.expected_fees, + &mut expected_fees, + human_to_dec("0.000001", Decimals::Six), + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 2, + "wrong number of denoms in contract balances" + ); + + wasm.execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: ATOM.to_string(), + min_output_quantity: FPDecimal::from(2800u128), + }, + &[coin(12, ETH)], + &swapper, + ) + .unwrap(); + + let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + assert_eq!( + from_balance, + FPDecimal::ZERO, + "some of the original amount wasn't swapped" + ); + assert_eq!( + to_balance, + FPDecimal::must_from_str("2889"), + "swapper did not receive expected amount" + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 2, + "wrong number of denoms in contract balances" + ); + assert_eq!( + contract_balances_after, contract_balances_before, + "contract balance has changed after swap" + ); +} + +#[test] +fn it_reverts_swap_between_markets_using_different_quote_asset_if_one_quote_buffer_is_insufficient() +{ + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = init_default_signer_account(&app); + let _validator = init_default_validator_account(&app); + let owner = init_rich_account(&app); + + let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); + let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDC); + let spot_market_3_id = launch_spot_market(&exchange, &owner, USDC, USDT); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[ + str_coin("0.0001", USDC, Decimals::Six), + str_coin("100_000", USDT, Decimals::Six), + ], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_3_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + create_eth_buy_orders(&app, &spot_market_1_id, &trader1, &trader2); + create_atom_sell_orders(&app, &spot_market_2_id, &trader1, &trader2, &trader3); + + //USDT-USDC + create_limit_order( + &app, + &trader3, + &spot_market_3_id, + OrderSide::Sell, + 1, + 100_000_000, + ); + + app.increase_time(1); + + let swapper = must_init_account_with_funds( + &app, + &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], + ); + + let query_result: RunnerResult = wasm.query( + &contr_addr, + &QueryMsg::GetOutputQuantity { + source_denom: ETH.to_string(), + target_denom: ATOM.to_string(), + from_quantity: FPDecimal::from(12u128), + }, + ); + + assert!(query_result.is_err(), "swap should have failed"); + assert!( + query_result + .unwrap_err() + .to_string() + .contains("Swap amount too high"), + "incorrect query result error message" + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 2, + "wrong number of denoms in contract balances" + ); + + let execute_result = wasm.execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: ATOM.to_string(), + min_output_quantity: FPDecimal::from(2800u128), + }, + &[coin(12, ETH)], + &swapper, + ); + + assert!(execute_result.is_err(), "swap should have failed"); + assert!( + execute_result + .unwrap_err() + .to_string() + .contains("Swap amount too high"), + "incorrect query result error message" + ); + + let source_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); + let target_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + assert_eq!( + source_balance, + FPDecimal::must_from_str("12"), + "source balance should not have changed after failed swap" + ); + assert_eq!( + target_balance, + FPDecimal::ZERO, + "target balance should not have changed after failed swap" + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 2, + "wrong number of denoms in contract balances" + ); + assert_eq!( + contract_balances_after, contract_balances_before, + "contract balance has changed after swap" + ); +} + +#[test] +fn it_executes_a_sell_of_base_asset_to_receive_min_output_quantity() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = init_default_signer_account(&app); + let _validator = init_default_validator_account(&app); + let owner = init_rich_account(&app); + + let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("100_000", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + USDT, + vec![spot_market_1_id.as_str().into()], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + + create_eth_buy_orders(&app, &spot_market_1_id, &trader1, &trader2); + + app.increase_time(1); + + let swapper = must_init_account_with_funds( + &app, + &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], + ); + + let mut query_result: SwapEstimationResult = wasm + .query( + &contr_addr, + &QueryMsg::GetOutputQuantity { + source_denom: ETH.to_string(), + target_denom: USDT.to_string(), + from_quantity: FPDecimal::from(12u128), + }, + ) + .unwrap(); + + // calculate how much can be USDT can be bought for 12 ETH without fees + let orders_nominal_total_value = FPDecimal::from(201_000u128) * FPDecimal::from(5u128) + + FPDecimal::from(195_000u128) * FPDecimal::from(4u128) + + FPDecimal::from(192_000u128) * FPDecimal::from(3u128); + let expected_target_quantity = orders_nominal_total_value + * (FPDecimal::ONE + - FPDecimal::must_from_str(&format!( + "{}", + DEFAULT_TAKER_FEE * DEFAULT_ATOMIC_MULTIPLIER * DEFAULT_SELF_RELAYING_FEE_PART + ))); + + assert_eq!( + query_result.result_quantity, expected_target_quantity, + "incorrect swap result estimate returned by query" + ); + + let mut expected_fees = vec![FPCoin { + amount: FPDecimal::must_from_str("3541.5"), + denom: "usdt".to_string(), + }]; + + assert_fee_is_as_expected( + &mut query_result.expected_fees, + &mut expected_fees, + FPDecimal::must_from_str("0.000001"), + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + wasm.execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: USDT.to_string(), + min_output_quantity: FPDecimal::from(2357458u128), + }, + &[coin(12, ETH)], + &swapper, + ) + .unwrap(); + + let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, USDT, swapper.address().as_str()); + let expected_execute_result = expected_target_quantity.int(); + + assert_eq!( + from_balance, + FPDecimal::ZERO, + "some of the original amount wasn't swapped" + ); + assert_eq!( + to_balance, expected_execute_result, + "swapper did not receive expected amount" + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + assert_eq!( + contract_balances_after, contract_balances_before, + "contract balance has changed after swap" + ); +} + +#[test] +fn it_executes_a_buy_of_base_asset_to_receive_min_output_quantity() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = init_default_signer_account(&app); + let _validator = init_default_validator_account(&app); + let owner = init_rich_account(&app); + + let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("100_000", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + USDT, + vec![spot_market_1_id.as_str().into()], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + + create_limit_order( + &app, + &trader1, + &spot_market_1_id, + OrderSide::Sell, + 201_000, + 5, + ); + create_limit_order( + &app, + &trader2, + &spot_market_1_id, + OrderSide::Sell, + 195_000, + 4, + ); + create_limit_order( + &app, + &trader2, + &spot_market_1_id, + OrderSide::Sell, + 192_000, + 3, + ); + + app.increase_time(1); + + let swapper_usdt = 2_360_995; + let swapper = must_init_account_with_funds( + &app, + &[ + coin(swapper_usdt, USDT), + str_coin("500_000", INJ, Decimals::Eighteen), + ], + ); + + // calculate how much ETH we can buy with USDT we have + let available_usdt_after_fee = FPDecimal::from(swapper_usdt) + / (FPDecimal::ONE + + FPDecimal::must_from_str(&format!( + "{}", + DEFAULT_TAKER_FEE * DEFAULT_ATOMIC_MULTIPLIER * DEFAULT_SELF_RELAYING_FEE_PART + ))); + let usdt_left_for_most_expensive_order = available_usdt_after_fee + - (FPDecimal::from(195_000u128) * FPDecimal::from(4u128) + + FPDecimal::from(192_000u128) * FPDecimal::from(3u128)); + let most_expensive_order_quantity = + usdt_left_for_most_expensive_order / FPDecimal::from(201_000u128); + let expected_quantity = + most_expensive_order_quantity + (FPDecimal::from(4u128) + FPDecimal::from(3u128)); + + // round to min tick + let expected_quantity_rounded = + round_to_min_tick(expected_quantity, FPDecimal::must_from_str("0.001")); + + // calculate dust notional value as this will be the portion of user's funds that will stay in the contract + let dust = expected_quantity - expected_quantity_rounded; + // we need to use worst priced order + let dust_value = dust * FPDecimal::from(201_000u128); + + let mut query_result: SwapEstimationResult = wasm + .query( + &contr_addr, + &QueryMsg::GetOutputQuantity { + source_denom: USDT.to_string(), + target_denom: ETH.to_string(), + from_quantity: FPDecimal::from(swapper_usdt), + }, + ) + .unwrap(); + + assert_eq!( + query_result.result_quantity, expected_quantity_rounded, + "incorrect swap result estimate returned by query" + ); + + let mut expected_fees = vec![FPCoin { + amount: FPDecimal::must_from_str("3536.188217"), + denom: "usdt".to_string(), + }]; + + assert_fee_is_as_expected( + &mut query_result.expected_fees, + &mut expected_fees, + FPDecimal::must_from_str("0.000001"), + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + wasm.execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: ETH.to_string(), + min_output_quantity: FPDecimal::from(11u128), + }, + &[coin(swapper_usdt, USDT)], + &swapper, + ) + .unwrap(); + + let from_balance = query_bank_balance(&bank, USDT, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); + let expected_execute_result = expected_quantity.int(); + + assert_eq!( + from_balance, + FPDecimal::ZERO, + "some of the original amount wasn't swapped" + ); + assert_eq!( + to_balance, expected_execute_result, + "swapper did not receive expected amount" + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + let mut expected_contract_balances_after = + FPDecimal::must_from_str(contract_balances_before[0].amount.as_str()) + dust_value; + expected_contract_balances_after = expected_contract_balances_after.int(); + + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + assert_eq!( + FPDecimal::must_from_str(contract_balances_after[0].amount.as_str()), + expected_contract_balances_after, + "contract balance changed unexpectedly after swap" + ); +} + +#[test] +fn it_executes_a_swap_between_base_assets_with_external_fee_recipient() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = init_default_signer_account(&app); + let _validator = init_default_validator_account(&app); + let owner = init_rich_account(&app); + + let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); + let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); + + let fee_recipient = must_init_account_with_funds(&app, &[]); + let contr_addr = init_contract_with_fee_recipient_and_get_address( + &wasm, + &owner, + &[str_coin("10_000", USDT, Decimals::Six)], + &fee_recipient, + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + create_eth_buy_orders(&app, &spot_market_1_id, &trader1, &trader2); + create_atom_sell_orders(&app, &spot_market_2_id, &trader1, &trader2, &trader3); + + // calculate relayer's share of the fee based on assumptions that all orders are matched + let buy_orders_nominal_total_value = FPDecimal::from(201_000u128) * FPDecimal::from(5u128) + + FPDecimal::from(195_000u128) * FPDecimal::from(4u128) + + FPDecimal::from(192_000u128) * FPDecimal::from(3u128); + let relayer_sell_fee = buy_orders_nominal_total_value + * FPDecimal::must_from_str(&format!( + "{}", + DEFAULT_TAKER_FEE * DEFAULT_ATOMIC_MULTIPLIER * DEFAULT_RELAYER_SHARE + )); + + // calculate relayer's share of the fee based on assumptions that some of orders are matched + let expected_nominal_buy_most_expensive_match_quantity = + FPDecimal::must_from_str("488.2222155454736648"); + let sell_orders_nominal_total_value = FPDecimal::from(800u128) * FPDecimal::from(800u128) + + FPDecimal::from(810u128) * FPDecimal::from(800u128) + + FPDecimal::from(820u128) * FPDecimal::from(800u128) + + FPDecimal::from(830u128) * expected_nominal_buy_most_expensive_match_quantity; + let relayer_buy_fee = sell_orders_nominal_total_value + * FPDecimal::must_from_str(&format!( + "{}", + DEFAULT_TAKER_FEE * DEFAULT_ATOMIC_MULTIPLIER * DEFAULT_RELAYER_SHARE + )); + let expected_fee_for_fee_recipient = relayer_buy_fee + relayer_sell_fee; + + app.increase_time(1); + + let swapper = must_init_account_with_funds( + &app, + &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], + ); + + let mut query_result: SwapEstimationResult = wasm + .query( + &contr_addr, + &QueryMsg::GetOutputQuantity { + source_denom: ETH.to_string(), + target_denom: ATOM.to_string(), + from_quantity: FPDecimal::from(12u128), + }, + ) + .unwrap(); + + assert_eq!( + query_result.result_quantity, + FPDecimal::must_from_str("2888.221"), //slightly rounded down vs spreadsheet + "incorrect swap result estimate returned by query" + ); + + let mut expected_fees = vec![ + FPCoin { + amount: FPDecimal::must_from_str("5902.5"), + denom: "usdt".to_string(), + }, + FPCoin { + amount: FPDecimal::must_from_str("5873.061097"), + denom: "usdt".to_string(), + }, + ]; + + assert_fee_is_as_expected( + &mut query_result.expected_fees, + &mut expected_fees, + FPDecimal::must_from_str("0.000001"), + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + wasm.execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: ATOM.to_string(), + min_output_quantity: FPDecimal::from(2888u128), + }, + &[coin(12, ETH)], + &swapper, + ) + .unwrap(); + + let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + + assert_eq!( + from_balance, + FPDecimal::ZERO, + "some of the original amount wasn't swapped" + ); + assert_eq!( + to_balance, + FPDecimal::must_from_str("2888"), + "swapper did not receive expected amount" + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let contract_balance_usdt_after = + FPDecimal::must_from_str(contract_balances_after[0].amount.as_str()); + let contract_balance_usdt_before = + FPDecimal::must_from_str(contract_balances_before[0].amount.as_str()); + + assert!( + contract_balance_usdt_after >= contract_balance_usdt_before, + "Contract lost some money after swap. Balance before: {contract_balance_usdt_before}, after: {contract_balance_usdt_after}", + ); + + let max_diff = human_to_dec("0.00001", Decimals::Six); + + assert!( + are_fpdecimals_approximately_equal( + contract_balance_usdt_after, + contract_balance_usdt_before, + max_diff, + ), + "Contract balance changed too much. Before: {}, After: {}", + contract_balances_before[0].amount, + contract_balances_after[0].amount + ); + + let fee_recipient_balance = query_all_bank_balances(&bank, &fee_recipient.address()); + + assert_eq!( + fee_recipient_balance.len(), + 1, + "wrong number of denoms in fee recipient's balances" + ); + assert_eq!( + fee_recipient_balance[0].denom, USDT, + "fee recipient did not receive fee in expected denom" + ); + assert_eq!( + FPDecimal::must_from_str(fee_recipient_balance[0].amount.as_str()), + expected_fee_for_fee_recipient.int(), + "fee recipient did not receive expected fee" + ); +} + +#[test] +fn it_reverts_the_swap_if_there_isnt_enough_buffer_for_buying_target_asset() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = init_default_signer_account(&app); + let _validator = init_default_validator_account(&app); + let owner = init_rich_account(&app); + + let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); + let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("0.001", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + create_eth_buy_orders(&app, &spot_market_1_id, &trader1, &trader2); + create_atom_sell_orders(&app, &spot_market_2_id, &trader1, &trader2, &trader3); + + app.increase_time(1); + + let swapper = must_init_account_with_funds( + &app, + &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], + ); + + let query_result: RunnerResult = wasm.query( + &contr_addr, + &QueryMsg::GetOutputQuantity { + source_denom: ETH.to_string(), + target_denom: ATOM.to_string(), + from_quantity: FPDecimal::from(12u128), + }, + ); + + assert!(query_result.is_err(), "query should fail"); + assert!( + query_result + .unwrap_err() + .to_string() + .contains("Swap amount too high"), + "wrong query error message" + ); + + let contract_balances_before = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let execute_result = wasm.execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: ATOM.to_string(), + min_output_quantity: FPDecimal::from(2800u128), + }, + &[coin(12, ETH)], + &swapper, + ); + + assert!(execute_result.is_err(), "execute should fail"); + assert!( + execute_result + .unwrap_err() + .to_string() + .contains("Swap amount too high"), + "wrong execute error message" + ); + + let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + assert_eq!( + from_balance, + FPDecimal::from(12u128), + "source balance changes after failed swap" + ); + assert_eq!( + to_balance, + FPDecimal::ZERO, + "target balance changes after failed swap" + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + assert_eq!( + contract_balances_after, contract_balances_before, + "contract balance has changed after swap" + ); +} + +#[test] +fn it_reverts_swap_if_no_funds_were_passed() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = init_default_signer_account(&app); + let _validator = init_default_validator_account(&app); + let owner = init_rich_account(&app); + + let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); + let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("100_000", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let swapper = must_init_account_with_funds( + &app, + &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], + ); + + let contract_balances_before = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let execute_result = wasm.execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: ATOM.to_string(), + min_output_quantity: FPDecimal::from(2800u128), + }, + &[], + &swapper, + ); + let expected_error = RunnerError::ExecuteError { msg: "failed to execute message; message index: 0: Custom Error: \"Only one denom can be passed in funds\": execute wasm contract failed".to_string() }; + assert_eq!( + execute_result.unwrap_err(), + expected_error, + "wrong error message" + ); + + let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + + assert_eq!( + from_balance, + FPDecimal::from(12u128), + "source balance changes after failed swap" + ); + assert_eq!( + to_balance, + FPDecimal::ZERO, + "target balance changes after failed swap" + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + assert_eq!( + contract_balances_after, contract_balances_before, + "contract balance has changed after swap" + ); +} + +#[test] +fn it_reverts_swap_if_multiple_funds_were_passed() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = init_default_signer_account(&app); + let _validator = init_default_validator_account(&app); + let owner = init_rich_account(&app); + + let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); + let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("100_000", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let eth_balance = 12u128; + let atom_balance = 10u128; + + let swapper = must_init_account_with_funds( + &app, + &[ + coin(eth_balance, ETH), + coin(atom_balance, ATOM), + str_coin("500_000", INJ, Decimals::Eighteen), + ], + ); + + let contract_balances_before = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let execute_result = wasm.execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: ATOM.to_string(), + min_output_quantity: FPDecimal::from(10u128), + }, + &[coin(10, ATOM), coin(12, ETH)], + &swapper, + ); + assert!( + execute_result + .unwrap_err() + .to_string() + .contains("Only one denom can be passed in funds"), + "wrong error message" + ); + + let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + assert_eq!( + from_balance, + FPDecimal::from(eth_balance), + "wrong ETH balance after failed swap" + ); + assert_eq!( + to_balance, + FPDecimal::from(atom_balance), + "wrong ATOM balance after failed swap" + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + assert_eq!( + contract_balances_after, contract_balances_before, + "contract balance has changed after swap" + ); +} + +#[test] +fn it_reverts_if_user_passes_quantities_equal_to_zero() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = init_default_signer_account(&app); + let _validator = init_default_validator_account(&app); + let owner = init_rich_account(&app); + + let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); + let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("100_000", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + app.increase_time(1); + + let swapper = must_init_account_with_funds( + &app, + &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], + ); + + let query_result: RunnerResult = wasm.query( + &contr_addr, + &QueryMsg::GetOutputQuantity { + source_denom: ETH.to_string(), + target_denom: ATOM.to_string(), + from_quantity: FPDecimal::from(0u128), + }, + ); + assert!( + query_result + .unwrap_err() + .to_string() + .contains("source_quantity must be positive"), + "incorrect error returned by query" + ); + + let contract_balances_before = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let err = wasm + .execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: ATOM.to_string(), + min_output_quantity: FPDecimal::ZERO, + }, + &[coin(12, ETH)], + &swapper, + ) + .unwrap_err(); + assert!( + err.to_string() + .contains("Output quantity must be positive!"), + "incorrect error returned by execute" + ); + + let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + assert_eq!( + from_balance, + FPDecimal::must_from_str("12"), + "swap should not have occurred" + ); + assert_eq!( + to_balance, + FPDecimal::must_from_str("0"), + "swapper should not have received any target tokens" + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + assert_eq!( + contract_balances_after, contract_balances_before, + "contract balance has changed after swap" + ); +} + +#[test] +fn it_reverts_if_user_passes_negative_quantities() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = init_default_signer_account(&app); + let _validator = init_default_validator_account(&app); + let owner = init_rich_account(&app); + + let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); + let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("100_000", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let contract_balances_before = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let swapper = must_init_account_with_funds( + &app, + &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], + ); + + app.increase_time(1); + + let execute_result = wasm.execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: ATOM.to_string(), + min_output_quantity: FPDecimal::must_from_str("-1"), + }, + &[coin(12, ETH)], + &swapper, + ); + + assert!( + execute_result.is_err(), + "swap with negative minimum amount to receive did not fail" + ); + + let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + assert_eq!( + from_balance, + FPDecimal::from(12u128), + "source balance changed after failed swap" + ); + assert_eq!( + to_balance, + FPDecimal::ZERO, + "target balance changed after failed swap" + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + assert_eq!( + contract_balances_after, contract_balances_before, + "contract balance has changed after failed swap" + ); +} + +#[test] +fn it_reverts_if_there_arent_enough_orders_to_satisfy_min_quantity() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = init_default_signer_account(&app); + let _validator = init_default_validator_account(&app); + let owner = init_rich_account(&app); + + let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); + let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("100_000", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + create_eth_buy_orders(&app, &spot_market_1_id, &trader1, &trader2); + + create_limit_order(&app, &trader1, &spot_market_2_id, OrderSide::Sell, 800, 800); + create_limit_order(&app, &trader2, &spot_market_2_id, OrderSide::Sell, 810, 800); + create_limit_order(&app, &trader3, &spot_market_2_id, OrderSide::Sell, 820, 800); + create_limit_order(&app, &trader1, &spot_market_2_id, OrderSide::Sell, 830, 450); //not enough for minimum requested + + app.increase_time(1); + + let swapper = must_init_account_with_funds( + &app, + &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], + ); + + let query_result: RunnerResult = wasm.query( + &contr_addr, + &QueryMsg::GetOutputQuantity { + source_denom: ETH.to_string(), + target_denom: ATOM.to_string(), + from_quantity: FPDecimal::from(12u128), + }, + ); + assert_eq!( + query_result.unwrap_err(), + QueryError { + msg: "Generic error: Not enough liquidity to fulfill order: query wasm contract failed" + .to_string() + }, + "wrong error message" + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let execute_result = wasm.execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: ATOM.to_string(), + min_output_quantity: FPDecimal::from(2800u128), + }, + &[coin(12, ETH)], + &swapper, + ); + + assert_eq!(execute_result.unwrap_err(), RunnerError::ExecuteError { msg: "failed to execute message; message index: 0: dispatch: submessages: reply: Generic error: Not enough liquidity to fulfill order: execute wasm contract failed".to_string() }, "wrong error message"); + + let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + assert_eq!( + from_balance, + FPDecimal::from(12u128), + "source balance changed after failed swap" + ); + assert_eq!( + to_balance, + FPDecimal::ZERO, + "target balance changed after failed swap" + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + assert_eq!( + contract_balances_after, contract_balances_before, + "contract balance has changed after swap" + ); +} + +#[test] +fn it_reverts_if_min_quantity_cannot_be_reached() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = init_default_signer_account(&app); + let _validator = init_default_validator_account(&app); + let owner = init_rich_account(&app); + + // set the market + let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); + let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("100_000", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + create_eth_buy_orders(&app, &spot_market_1_id, &trader1, &trader2); + create_atom_sell_orders(&app, &spot_market_2_id, &trader1, &trader2, &trader3); + + app.increase_time(1); + + let swapper = must_init_account_with_funds( + &app, + &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let min_quantity = 3500u128; + let execute_result = wasm.execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: ATOM.to_string(), + min_output_quantity: FPDecimal::from(min_quantity), + }, + &[coin(12, ETH)], + &swapper, + ); + + assert_eq!(execute_result.unwrap_err(), RunnerError::ExecuteError { msg: format!("failed to execute message; message index: 0: dispatch: submessages: reply: dispatch: submessages: reply: Min expected swap amount ({min_quantity}) not reached: execute wasm contract failed") }, "wrong error message"); + + let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + assert_eq!( + from_balance, + FPDecimal::from(12u128), + "source balance changed after failed swap" + ); + assert_eq!( + to_balance, + FPDecimal::ZERO, + "target balance changed after failed swap" + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + assert_eq!( + contract_balances_after, contract_balances_before, + "contract balance has changed after failed swap" + ); +} + +#[test] +fn it_reverts_if_market_is_paused() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let signer = init_default_signer_account(&app); + let validator = init_default_validator_account(&app); + fund_account_with_some_inj(&bank, &signer, &validator); + let owner = init_rich_account(&app); + + let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); + let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); + + pause_spot_market(&app, spot_market_1_id.as_str(), &signer, &validator); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("100_000", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let swapper = must_init_account_with_funds( + &app, + &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], + ); + + let query_error: RunnerError = wasm + .query::( + &contr_addr, + &QueryMsg::GetOutputQuantity { + source_denom: ETH.to_string(), + target_denom: ATOM.to_string(), + from_quantity: FPDecimal::from(12u128), + }, + ) + .unwrap_err(); + + assert!( + query_error.to_string().contains("Querier contract error"), + "wrong error returned by query" + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let execute_result = wasm.execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: ATOM.to_string(), + min_output_quantity: FPDecimal::from(2800u128), + }, + &[coin(12, ETH)], + &swapper, + ); + + assert!( + execute_result + .unwrap_err() + .to_string() + .contains("Querier contract error"), + "wrong error returned by execute" + ); + + let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + assert_eq!( + from_balance, + FPDecimal::from(12u128), + "source balance changed after failed swap" + ); + assert_eq!( + to_balance, + FPDecimal::ZERO, + "target balance changed after failed swap" + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + assert_eq!( + contract_balances_after, contract_balances_before, + "contract balance has changed after failed swap" + ); +} + +#[test] +fn it_reverts_if_user_doesnt_have_enough_inj_to_pay_for_gas() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = init_default_signer_account(&app); + let _validator = init_default_validator_account(&app); + let owner = init_rich_account(&app); + + let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); + let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("100_000", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let swapper = must_init_account_with_funds(&app, &[coin(12, ETH), coin(10, INJ)]); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + create_eth_buy_orders(&app, &spot_market_1_id, &trader1, &trader2); + create_atom_sell_orders(&app, &spot_market_2_id, &trader1, &trader2, &trader3); + + app.increase_time(1); + + let query_result: RunnerResult = wasm.query( + &contr_addr, + &QueryMsg::GetOutputQuantity { + source_denom: ETH.to_string(), + target_denom: ATOM.to_string(), + from_quantity: FPDecimal::from(12u128), + }, + ); + + let target_quantity = query_result.unwrap().result_quantity; + + assert_eq!( + target_quantity, + FPDecimal::must_from_str("2893.886"), //slightly underestimated vs spreadsheet + "incorrect swap result estimate returned by query" + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let execute_result = wasm.execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: ATOM.to_string(), + min_output_quantity: FPDecimal::from(2800u128), + }, + &[coin(12, ETH)], + &swapper, + ); + + assert_eq!(execute_result.unwrap_err(), ExecuteError { msg: "spendable balance 10inj is smaller than 2500inj: insufficient funds: insufficient funds".to_string() }, "wrong error returned by execute"); + + let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + assert_eq!( + from_balance, + FPDecimal::from(12u128), + "source balance changed after failed swap" + ); + assert_eq!( + to_balance, + FPDecimal::ZERO, + "target balance changed after failed swap" + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + assert_eq!( + contract_balances_after, contract_balances_before, + "contract balance has changed after failed swap" + ); +} + +#[test] +fn it_reverts_if_target_quantity_is_not_multiple_of_min_quantity_tick_size() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = init_default_signer_account(&app); + let _validator = init_default_validator_account(&app); + let owner = init_rich_account(&app); + + // set the market + let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); + let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("100_000", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + create_eth_buy_orders(&app, &spot_market_1_id, &trader1, &trader2); + create_atom_sell_orders(&app, &spot_market_2_id, &trader1, &trader2, &trader3); + + app.increase_time(1); + + let swapper = must_init_account_with_funds( + &app, + &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let min_quantity = FPDecimal::must_from_str("3500.0001"); + let execute_result = wasm.execute( + &contr_addr, + &ExecuteMsg::SwapExactOutput { + target_denom: ATOM.to_string(), + target_output_quantity: min_quantity, + }, + &[coin(12, ETH)], + &swapper, + ); + + assert_eq!(execute_result.unwrap_err(), RunnerError::ExecuteError { msg: "failed to execute message; message index: 0: Generic error: Target quantity must be a multiple of min_quantity_tick_size: execute wasm contract failed".to_string() }, "wrong error message"); + + let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + assert_eq!( + from_balance, + FPDecimal::from(12u128), + "source balance changed after failed swap" + ); + assert_eq!( + to_balance, + FPDecimal::ZERO, + "target balance changed after failed swap" + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + assert_eq!( + contract_balances_after, contract_balances_before, + "contract balance has changed after failed swap" + ); +} + +#[test] +fn it_allows_admin_to_withdraw_all_funds_from_contract_to_his_address() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let bank = Bank::new(&app); + + let usdt_to_withdraw = str_coin("10_000", USDT, Decimals::Six); + let eth_to_withdraw = str_coin("0.00062", ETH, Decimals::Eighteen); + + let owner = must_init_account_with_funds( + &app, + &[ + eth_to_withdraw.clone(), + str_coin("1", INJ, Decimals::Eighteen), + usdt_to_withdraw.clone(), + ], + ); + + let initial_contract_balance = &[eth_to_withdraw, usdt_to_withdraw]; + let contr_addr = + init_self_relaying_contract_and_get_address(&wasm, &owner, initial_contract_balance); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 2, + "wrong number of denoms in contract balances" + ); + + let execute_result = wasm.execute( + &contr_addr, + &ExecuteMsg::WithdrawSupportFunds { + coins: initial_contract_balance.to_vec(), + target_address: Addr::unchecked(owner.address()), + }, + &[], + &owner, + ); + + assert!(execute_result.is_ok(), "failed to withdraw support funds"); + let contract_balances_after = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_after.len(), + 0, + "contract had some balances after withdraw" + ); + + let owner_eth_balance = query_bank_balance(&bank, ETH, owner.address().as_str()); + assert_eq!( + owner_eth_balance, + FPDecimal::from(initial_contract_balance[0].amount), + "wrong owner eth balance after withdraw" + ); + + let owner_usdt_balance = query_bank_balance(&bank, USDT, owner.address().as_str()); + assert_eq!( + owner_usdt_balance, + FPDecimal::from(initial_contract_balance[1].amount), + "wrong owner usdt balance after withdraw" + ); +} + +#[test] +fn it_allows_admin_to_withdraw_all_funds_from_contract_to_other_address() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let bank = Bank::new(&app); + + let usdt_to_withdraw = str_coin("10_000", USDT, Decimals::Six); + let eth_to_withdraw = str_coin("0.00062", ETH, Decimals::Eighteen); + + let owner = must_init_account_with_funds( + &app, + &[ + eth_to_withdraw.clone(), + str_coin("1", INJ, Decimals::Eighteen), + usdt_to_withdraw.clone(), + ], + ); + + let initial_contract_balance = &[eth_to_withdraw, usdt_to_withdraw]; + let contr_addr = + init_self_relaying_contract_and_get_address(&wasm, &owner, initial_contract_balance); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 2, + "wrong number of denoms in contract balances" + ); + + let random_dude = must_init_account_with_funds(&app, &[]); + + let execute_result = wasm.execute( + &contr_addr, + &ExecuteMsg::WithdrawSupportFunds { + coins: initial_contract_balance.to_vec(), + target_address: Addr::unchecked(random_dude.address()), + }, + &[], + &owner, + ); + + assert!(execute_result.is_ok(), "failed to withdraw support funds"); + let contract_balances_after = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_after.len(), + 0, + "contract had some balances after withdraw" + ); + + let random_dude_eth_balance = query_bank_balance(&bank, ETH, random_dude.address().as_str()); + assert_eq!( + random_dude_eth_balance, + FPDecimal::from(initial_contract_balance[0].amount), + "wrong owner eth balance after withdraw" + ); + + let random_dude_usdt_balance = query_bank_balance(&bank, USDT, random_dude.address().as_str()); + assert_eq!( + random_dude_usdt_balance, + FPDecimal::from(initial_contract_balance[1].amount), + "wrong owner usdt balance after withdraw" + ); +} + +#[test] +fn it_doesnt_allow_non_admin_to_withdraw_anything_from_contract() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let bank = Bank::new(&app); + + let usdt_to_withdraw = str_coin("10_000", USDT, Decimals::Six); + let eth_to_withdraw = str_coin("0.00062", ETH, Decimals::Eighteen); + + let owner = must_init_account_with_funds( + &app, + &[ + eth_to_withdraw.clone(), + str_coin("1", INJ, Decimals::Eighteen), + usdt_to_withdraw.clone(), + ], + ); + + let initial_contract_balance = &[eth_to_withdraw, usdt_to_withdraw]; + let contr_addr = + init_self_relaying_contract_and_get_address(&wasm, &owner, initial_contract_balance); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 2, + "wrong number of denoms in contract balances" + ); + + let random_dude = must_init_account_with_funds(&app, &[coin(1_000_000_000_000, INJ)]); + + let execute_result = wasm.execute( + &contr_addr, + &ExecuteMsg::WithdrawSupportFunds { + coins: initial_contract_balance.to_vec(), + target_address: Addr::unchecked(owner.address()), + }, + &[], + &random_dude, + ); + + assert!( + execute_result.is_err(), + "succeeded to withdraw support funds" + ); + let contract_balances_after = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_after, contract_balances_before, + "contract balances changed after failed withdraw" + ); + + let random_dude_eth_balance = query_bank_balance(&bank, ETH, random_dude.address().as_str()); + assert_eq!( + random_dude_eth_balance, + FPDecimal::ZERO, + "random dude has some eth balance after failed withdraw" + ); + + let random_dude_usdt_balance = query_bank_balance(&bank, USDT, random_dude.address().as_str()); + assert_eq!( + random_dude_usdt_balance, + FPDecimal::ZERO, + "random dude has some usdt balance after failed withdraw" + ); +} + +fn create_eth_buy_orders( + app: &InjectiveTestApp, + market_id: &str, + trader1: &SigningAccount, + trader2: &SigningAccount, +) { + create_limit_order(app, trader1, market_id, OrderSide::Buy, 201_000, 5); + create_limit_order(app, trader2, market_id, OrderSide::Buy, 195_000, 4); + create_limit_order(app, trader2, market_id, OrderSide::Buy, 192_000, 3); +} + +fn create_atom_sell_orders( + app: &InjectiveTestApp, + market_id: &str, + trader1: &SigningAccount, + trader2: &SigningAccount, + trader3: &SigningAccount, +) { + create_limit_order(app, trader1, market_id, OrderSide::Sell, 800, 800); + create_limit_order(app, trader2, market_id, OrderSide::Sell, 810, 800); + create_limit_order(app, trader3, market_id, OrderSide::Sell, 820, 800); + create_limit_order(app, trader1, market_id, OrderSide::Sell, 830, 800); +} diff --git a/contracts/swap/src/testing/integration_tests/integration_realistic_tests_exact_quantity.rs b/contracts/swap/src/testing/integration_tests/integration_realistic_tests_exact_quantity.rs new file mode 100644 index 0000000..53e129f --- /dev/null +++ b/contracts/swap/src/testing/integration_tests/integration_realistic_tests_exact_quantity.rs @@ -0,0 +1,1685 @@ +use injective_test_tube::{Account, Bank, Exchange, InjectiveTestApp, Module, Wasm}; +use std::ops::Neg; + +use crate::helpers::Scaled; +use injective_math::FPDecimal; + +use crate::msg::{ExecuteMsg, QueryMsg}; +use crate::testing::test_utils::{ + are_fpdecimals_approximately_equal, assert_fee_is_as_expected, + create_realistic_atom_usdt_sell_orders_from_spreadsheet, + create_realistic_eth_usdt_buy_orders_from_spreadsheet, + create_realistic_eth_usdt_sell_orders_from_spreadsheet, + create_realistic_inj_usdt_buy_orders_from_spreadsheet, create_realistic_limit_order, + create_realistic_usdt_usdc_both_side_orders, human_to_dec, init_rich_account, + init_self_relaying_contract_and_get_address, launch_realistic_atom_usdt_spot_market, + launch_realistic_inj_usdt_spot_market, launch_realistic_usdt_usdc_spot_market, + launch_realistic_weth_usdt_spot_market, must_init_account_with_funds, query_all_bank_balances, + query_bank_balance, set_route_and_assert_success, str_coin, Decimals, OrderSide, ATOM, ETH, + INJ, INJ_2, USDC, USDT, +}; +use crate::types::{FPCoin, SwapEstimationResult}; + +/* + This test suite focuses on using using realistic values both for spot markets and for orders and + focuses on swaps requesting exact amount. This works as expected apart, when we are converting very + low quantities from a source asset that is orders of magnitude more expensive than the target + asset (as we round up to min quantity tick size). + + ATOM/USDT market parameters was taken from mainnet. ETH/USDT market parameters mirror WETH/USDT + spot market on mainnet. INJ_2/USDT mirrors mainnet's INJ/USDT market (we used a different denom + to avoid mixing balance changes related to gas payments). + + All values used in these tests come from the 2nd, 3rd and 4th tab of this spreadsheet: + https://docs.google.com/spreadsheets/d/1-0epjX580nDO_P2mm1tSjhvjJVppsvrO1BC4_wsBeyA/edit?usp=sharing + + In all tests contract is configured to self-relay trades and thus receive a 60% fee discount. +*/ + +struct Percent<'a>(&'a str); + +#[test] +fn it_swaps_eth_to_get_minimum_exact_amount_of_atom_by_mildly_rounding_up() { + exact_two_hop_eth_atom_swap_test_template(human_to_dec("0.01", Decimals::Six), Percent("2200")) +} + +#[test] +fn it_swaps_eth_to_get_very_low_exact_amount_of_atom_by_heavily_rounding_up() { + exact_two_hop_eth_atom_swap_test_template(human_to_dec("0.11", Decimals::Six), Percent("110")) +} + +#[test] +fn it_swaps_eth_to_get_low_exact_amount_of_atom_by_rounding_up() { + exact_two_hop_eth_atom_swap_test_template(human_to_dec("4.12", Decimals::Six), Percent("10")) +} + +#[test] +fn it_correctly_swaps_eth_to_get_normal_exact_amount_of_atom() { + exact_two_hop_eth_atom_swap_test_template(human_to_dec("12.05", Decimals::Six), Percent("1")) +} + +#[test] +fn it_correctly_swaps_eth_to_get_high_exact_amount_of_atom() { + exact_two_hop_eth_atom_swap_test_template(human_to_dec("612", Decimals::Six), Percent("1")) +} + +#[test] +fn it_correctly_swaps_eth_to_get_very_high_exact_amount_of_atom() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = must_init_account_with_funds(&app, &[str_coin("1", INJ, Decimals::Eighteen)]); + + let _validator = app + .get_first_validator_signing_account(INJ.to_string(), 1.2f64) + .unwrap(); + let owner = must_init_account_with_funds( + &app, + &[ + str_coin("1", ETH, Decimals::Eighteen), + str_coin("1", ATOM, Decimals::Six), + str_coin("1_000", USDT, Decimals::Six), + str_coin("10_000", INJ, Decimals::Eighteen), + ], + ); + + let spot_market_1_id = launch_realistic_weth_usdt_spot_market(&exchange, &owner); + let spot_market_2_id = launch_realistic_atom_usdt_spot_market(&exchange, &owner); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("1_000", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + create_realistic_eth_usdt_buy_orders_from_spreadsheet( + &app, + &spot_market_1_id, + &trader1, + &trader2, + ); + create_realistic_limit_order( + &app, + &trader1, + &spot_market_1_id, + OrderSide::Buy, + "2137.2", + "2.78", + Decimals::Eighteen, + Decimals::Six, + ); //order not present in the spreadsheet + + create_realistic_atom_usdt_sell_orders_from_spreadsheet( + &app, + &spot_market_2_id, + &trader1, + &trader2, + &trader3, + ); + create_realistic_limit_order( + &app, + &trader1, + &spot_market_2_id, + OrderSide::Sell, + "9.11", + "321.11", + Decimals::Six, + Decimals::Six, + ); //order not present in the spreadsheet + + app.increase_time(1); + + let eth_to_swap = "4.4"; + + let swapper = must_init_account_with_funds( + &app, + &[ + str_coin(eth_to_swap, ETH, Decimals::Eighteen), + str_coin("1", INJ, Decimals::Eighteen), + ], + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let exact_quantity_to_receive = human_to_dec("1014.19", Decimals::Six); + + let query_result: SwapEstimationResult = wasm + .query( + &contr_addr, + &QueryMsg::GetInputQuantity { + source_denom: ETH.to_string(), + target_denom: ATOM.to_string(), + to_quantity: exact_quantity_to_receive, + }, + ) + .unwrap(); + + wasm.execute( + &contr_addr, + &ExecuteMsg::SwapExactOutput { + target_denom: ATOM.to_string(), + target_output_quantity: exact_quantity_to_receive, + }, + &[str_coin(eth_to_swap, ETH, Decimals::Eighteen)], + &swapper, + ) + .unwrap(); + + let expected_difference = + human_to_dec(eth_to_swap, Decimals::Eighteen) - query_result.result_quantity; + let swapper_eth_balance_after = query_bank_balance(&bank, ETH, swapper.address().as_str()); + let swapper_atom_balance_after = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + + assert_eq!( + swapper_eth_balance_after, expected_difference, + "wrong amount of ETH was exchanged" + ); + + assert!( + swapper_atom_balance_after >= exact_quantity_to_receive, + "swapper got less than exact amount required -> expected: {} ATOM, actual: {} ATOM", + exact_quantity_to_receive.scaled(Decimals::Six.get_decimals().neg()), + swapper_atom_balance_after.scaled(Decimals::Six.get_decimals().neg()) + ); + + let one_percent_diff = exact_quantity_to_receive * FPDecimal::must_from_str("0.01"); + + assert!( + are_fpdecimals_approximately_equal( + swapper_atom_balance_after, + exact_quantity_to_receive, + one_percent_diff, + ), + "swapper did not receive expected exact amount +/- 1% -> expected: {} ATOM, actual: {} ATOM, max diff: {} ATOM", + exact_quantity_to_receive.scaled(Decimals::Six.get_decimals().neg()), + swapper_atom_balance_after.scaled(Decimals::Six.get_decimals().neg()), + one_percent_diff.scaled(Decimals::Six.get_decimals().neg()) + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let contract_usdt_balance_before = + FPDecimal::must_from_str(contract_balances_before[0].amount.as_str()); + let contract_usdt_balance_after = + FPDecimal::must_from_str(contract_balances_after[0].amount.as_str()); + + assert!( + contract_usdt_balance_after >= contract_usdt_balance_before, + "Contract lost some money after swap. Actual balance: {contract_usdt_balance_after}, previous balance: {contract_usdt_balance_before}", + ); + + // contract is allowed to earn extra 0.73 USDT from the swap of ~$8450 worth of ETH + let max_diff = human_to_dec("0.8", Decimals::Six); + + assert!( + are_fpdecimals_approximately_equal( + contract_usdt_balance_after, + contract_usdt_balance_before, + max_diff, + ), + "Contract balance changed too much. Actual balance: {}, previous balance: {}. Max diff: {}", + contract_usdt_balance_after.scaled(Decimals::Six.get_decimals().neg()), + contract_usdt_balance_before.scaled(Decimals::Six.get_decimals().neg()), + max_diff.scaled(Decimals::Six.get_decimals().neg()) + ); +} + +#[test] +fn it_swaps_inj_to_get_minimum_exact_amount_of_atom_by_mildly_rounding_up() { + exact_two_hop_inj_atom_swap_test_template(human_to_dec("0.01", Decimals::Six), Percent("0")) +} + +#[test] +fn it_swaps_inj_to_get_very_low_exact_amount_of_atom() { + exact_two_hop_inj_atom_swap_test_template(human_to_dec("0.11", Decimals::Six), Percent("0")) +} + +#[test] +fn it_swaps_inj_to_get_low_exact_amount_of_atom() { + exact_two_hop_inj_atom_swap_test_template(human_to_dec("4.12", Decimals::Six), Percent("0")) +} + +#[test] +fn it_correctly_swaps_inj_to_get_normal_exact_amount_of_atom() { + exact_two_hop_inj_atom_swap_test_template(human_to_dec("12.05", Decimals::Six), Percent("0")) +} + +#[test] +fn it_correctly_swaps_inj_to_get_high_exact_amount_of_atom() { + exact_two_hop_inj_atom_swap_test_template(human_to_dec("612", Decimals::Six), Percent("0.01")) +} + +#[test] +fn it_correctly_swaps_inj_to_get_very_high_exact_amount_of_atom() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = must_init_account_with_funds(&app, &[str_coin("1", INJ, Decimals::Eighteen)]); + + let _validator = app + .get_first_validator_signing_account(INJ.to_string(), 1.2f64) + .unwrap(); + let owner = must_init_account_with_funds( + &app, + &[ + str_coin("1", ETH, Decimals::Eighteen), + str_coin("1", ATOM, Decimals::Six), + str_coin("1_000", USDT, Decimals::Six), + str_coin("10_000", INJ, Decimals::Eighteen), + str_coin("10_000", INJ_2, Decimals::Eighteen), + ], + ); + + let spot_market_1_id = launch_realistic_inj_usdt_spot_market(&exchange, &owner); + let spot_market_2_id = launch_realistic_atom_usdt_spot_market(&exchange, &owner); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("1_000", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + INJ_2, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + create_realistic_inj_usdt_buy_orders_from_spreadsheet( + &app, + &spot_market_1_id, + &trader1, + &trader2, + ); + create_realistic_limit_order( + &app, + &trader1, + &spot_market_1_id, + OrderSide::Buy, + "8.99", + "280.2", + Decimals::Eighteen, + Decimals::Six, + ); //order not present in the spreadsheet + + create_realistic_atom_usdt_sell_orders_from_spreadsheet( + &app, + &spot_market_2_id, + &trader1, + &trader2, + &trader3, + ); + create_realistic_limit_order( + &app, + &trader1, + &spot_market_2_id, + OrderSide::Sell, + "9.11", + "321.11", + Decimals::Six, + Decimals::Six, + ); //order not present in the spreadsheet + + app.increase_time(1); + + let inj_to_swap = "1100.1"; + + let swapper = must_init_account_with_funds( + &app, + &[ + str_coin(inj_to_swap, INJ_2, Decimals::Eighteen), + str_coin("1", INJ, Decimals::Eighteen), + ], + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let exact_quantity_to_receive = human_to_dec("1010.12", Decimals::Six); + let max_diff_percentage = Percent("0.01"); + + let query_result: SwapEstimationResult = wasm + .query( + &contr_addr, + &QueryMsg::GetInputQuantity { + source_denom: INJ_2.to_string(), + target_denom: ATOM.to_string(), + to_quantity: exact_quantity_to_receive, + }, + ) + .unwrap(); + + wasm.execute( + &contr_addr, + &ExecuteMsg::SwapExactOutput { + target_denom: ATOM.to_string(), + target_output_quantity: exact_quantity_to_receive, + }, + &[str_coin(inj_to_swap, INJ_2, Decimals::Eighteen)], + &swapper, + ) + .unwrap(); + + let expected_difference = + human_to_dec(inj_to_swap, Decimals::Eighteen) - query_result.result_quantity; + let swapper_inj_balance_after = query_bank_balance(&bank, INJ_2, swapper.address().as_str()); + let swapper_atom_balance_after = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + + assert_eq!( + swapper_inj_balance_after, expected_difference, + "wrong amount of INJ was exchanged" + ); + + assert!( + swapper_atom_balance_after >= exact_quantity_to_receive, + "swapper got less than exact amount required -> expected: {} ATOM, actual: {} ATOM", + exact_quantity_to_receive.scaled(Decimals::Six.get_decimals().neg()), + swapper_atom_balance_after.scaled(Decimals::Six.get_decimals().neg()) + ); + + let one_percent_diff = exact_quantity_to_receive + * (FPDecimal::must_from_str(max_diff_percentage.0) / FPDecimal::from(100u128)); + + assert!( + are_fpdecimals_approximately_equal( + swapper_atom_balance_after, + exact_quantity_to_receive, + one_percent_diff, + ), + "swapper did not receive expected exact ATOM amount +/- {}% -> expected: {} ATOM, actual: {} ATOM, max diff: {} ATOM", + max_diff_percentage.0, + exact_quantity_to_receive.scaled(Decimals::Six.get_decimals().neg()), + swapper_atom_balance_after.scaled(Decimals::Six.get_decimals().neg()), + one_percent_diff.scaled(Decimals::Six.get_decimals().neg()) + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let contract_usdt_balance_before = + FPDecimal::must_from_str(contract_balances_before[0].amount.as_str()); + let contract_usdt_balance_after = + FPDecimal::must_from_str(contract_balances_after[0].amount.as_str()); + + assert!( + contract_usdt_balance_after >= contract_usdt_balance_before, + "Contract lost some money after swap. Actual balance: {contract_usdt_balance_after}, previous balance: {contract_usdt_balance_before}", + ); + + // contract is allowed to earn extra 0.7 USDT from the swap of ~$8150 worth of INJ + let max_diff = human_to_dec("0.7", Decimals::Six); + + assert!( + are_fpdecimals_approximately_equal( + contract_usdt_balance_after, + contract_usdt_balance_before, + max_diff, + ), + "Contract balance changed too much. Actual balance: {} USDT, previous balance: {} USDT. Max diff: {} USDT", + contract_usdt_balance_after.scaled(Decimals::Six.get_decimals().neg()), + contract_usdt_balance_before.scaled(Decimals::Six.get_decimals().neg()), + max_diff.scaled(Decimals::Six.get_decimals().neg()) + ); +} + +#[test] +fn it_swaps_inj_to_get_minimum_exact_amount_of_eth() { + exact_two_hop_inj_eth_swap_test_template( + human_to_dec("0.001", Decimals::Eighteen), + Percent("0"), + ) +} + +#[test] +fn it_swaps_inj_to_get_low_exact_amount_of_eth() { + exact_two_hop_inj_eth_swap_test_template( + human_to_dec("0.012", Decimals::Eighteen), + Percent("0"), + ) +} + +#[test] +fn it_swaps_inj_to_get_normal_exact_amount_of_eth() { + exact_two_hop_inj_eth_swap_test_template(human_to_dec("0.1", Decimals::Eighteen), Percent("0")) +} + +#[test] +fn it_swaps_inj_to_get_high_exact_amount_of_eth() { + exact_two_hop_inj_eth_swap_test_template(human_to_dec("3.1", Decimals::Eighteen), Percent("0")) +} + +#[test] +fn it_swaps_inj_to_get_very_high_exact_amount_of_eth() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = must_init_account_with_funds(&app, &[str_coin("1", INJ, Decimals::Eighteen)]); + + let _validator = app + .get_first_validator_signing_account(INJ.to_string(), 1.2f64) + .unwrap(); + let owner = must_init_account_with_funds( + &app, + &[ + str_coin("1", ETH, Decimals::Eighteen), + str_coin("1_000", USDT, Decimals::Six), + str_coin("10_000", INJ, Decimals::Eighteen), + str_coin("10_000", INJ_2, Decimals::Eighteen), + ], + ); + + let spot_market_1_id = launch_realistic_inj_usdt_spot_market(&exchange, &owner); + let spot_market_2_id = launch_realistic_weth_usdt_spot_market(&exchange, &owner); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("1_000", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + INJ_2, + ETH, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + create_realistic_inj_usdt_buy_orders_from_spreadsheet( + &app, + &spot_market_1_id, + &trader1, + &trader2, + ); + create_realistic_limit_order( + &app, + &trader1, + &spot_market_1_id, + OrderSide::Buy, + "8.99", + "1882.001", + Decimals::Eighteen, + Decimals::Six, + ); //order not present in the spreadsheet + create_realistic_eth_usdt_sell_orders_from_spreadsheet( + &app, + &spot_market_2_id, + &trader1, + &trader2, + &trader3, + ); + create_realistic_limit_order( + &app, + &trader3, + &spot_market_2_id, + OrderSide::Sell, + "2123.1", + "18.11", + Decimals::Eighteen, + Decimals::Six, + ); //order not present in the spreadsheet + + app.increase_time(1); + + let inj_to_swap = "2855.259"; + let exact_quantity_to_receive = human_to_dec("11.2", Decimals::Eighteen); + + let swapper = must_init_account_with_funds( + &app, + &[ + str_coin(inj_to_swap, INJ_2, Decimals::Eighteen), + str_coin("1", INJ, Decimals::Eighteen), + ], + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let query_result: SwapEstimationResult = wasm + .query( + &contr_addr, + &QueryMsg::GetInputQuantity { + source_denom: INJ_2.to_string(), + target_denom: ETH.to_string(), + to_quantity: exact_quantity_to_receive, + }, + ) + .unwrap(); + + wasm.execute( + &contr_addr, + &ExecuteMsg::SwapExactOutput { + target_denom: ETH.to_string(), + target_output_quantity: exact_quantity_to_receive, + }, + &[str_coin(inj_to_swap, INJ_2, Decimals::Eighteen)], + &swapper, + ) + .unwrap(); + + let expected_difference = + human_to_dec(inj_to_swap, Decimals::Eighteen) - query_result.result_quantity; + let swapper_inj_balance_after = query_bank_balance(&bank, INJ_2, swapper.address().as_str()); + let swapper_atom_balance_after = query_bank_balance(&bank, ETH, swapper.address().as_str()); + + assert_eq!( + swapper_inj_balance_after, expected_difference, + "wrong amount of INJ was exchanged" + ); + + assert!( + swapper_atom_balance_after >= exact_quantity_to_receive, + "swapper got less than exact amount required -> expected: {} ETH, actual: {} ETH", + exact_quantity_to_receive.scaled(Decimals::Eighteen.get_decimals().neg()), + swapper_atom_balance_after.scaled(Decimals::Eighteen.get_decimals().neg()) + ); + + let max_diff_percent = Percent("0"); + let one_percent_diff = exact_quantity_to_receive + * (FPDecimal::must_from_str(max_diff_percent.0) / FPDecimal::from(100u128)); + + assert!( + are_fpdecimals_approximately_equal( + swapper_atom_balance_after, + exact_quantity_to_receive, + one_percent_diff, + ), + "swapper did not receive expected exact ETH amount +/- {}% -> expected: {} ETH, actual: {} ETH, max diff: {} ETH", + max_diff_percent.0, + exact_quantity_to_receive.scaled(Decimals::Eighteen.get_decimals().neg()), + swapper_atom_balance_after.scaled(Decimals::Eighteen.get_decimals().neg()), + one_percent_diff.scaled(Decimals::Eighteen.get_decimals().neg()) + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let contract_usdt_balance_before = + FPDecimal::must_from_str(contract_balances_before[0].amount.as_str()); + let contract_usdt_balance_after = + FPDecimal::must_from_str(contract_balances_after[0].amount.as_str()); + + assert!( + contract_usdt_balance_after >= contract_usdt_balance_before, + "Contract lost some money after swap. Actual balance: {contract_usdt_balance_after}, previous balance: {contract_usdt_balance_before}", + ); + + // contract is allowed to earn extra 1.6 USDT from the swap of ~$23500 worth of INJ + let max_diff = human_to_dec("1.6", Decimals::Six); + + assert!( + are_fpdecimals_approximately_equal( + contract_usdt_balance_after, + contract_usdt_balance_before, + max_diff, + ), + "Contract balance changed too much. Actual balance: {} USDT, previous balance: {} USDT. Max diff: {} USDT", + contract_usdt_balance_after.scaled(Decimals::Six.get_decimals().neg()), + contract_usdt_balance_before.scaled(Decimals::Six.get_decimals().neg()), + max_diff.scaled(Decimals::Six.get_decimals().neg()) + ); +} + +#[test] +fn it_correctly_swaps_between_markets_using_different_quote_assets_self_relaying() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = must_init_account_with_funds(&app, &[str_coin("1", INJ, Decimals::Eighteen)]); + let _validator = app + .get_first_validator_signing_account(INJ.to_string(), 1.2f64) + .unwrap(); + + let owner = must_init_account_with_funds( + &app, + &[ + str_coin("1_000", USDT, Decimals::Six), + str_coin("1_000", USDC, Decimals::Six), + str_coin("10_000", INJ, Decimals::Eighteen), + str_coin("1", INJ_2, Decimals::Eighteen), + ], + ); + + let spot_market_1_id = launch_realistic_inj_usdt_spot_market(&exchange, &owner); + let spot_market_2_id = launch_realistic_usdt_usdc_spot_market(&exchange, &owner); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[ + str_coin("10", USDC, Decimals::Six), + str_coin("500", USDT, Decimals::Six), + ], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + INJ_2, + USDC, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + + create_realistic_inj_usdt_buy_orders_from_spreadsheet( + &app, + &spot_market_1_id, + &trader1, + &trader2, + ); + create_realistic_usdt_usdc_both_side_orders(&app, &spot_market_2_id, &trader1); + + app.increase_time(1); + + let swapper = must_init_account_with_funds( + &app, + &[ + str_coin("1", INJ, Decimals::Eighteen), + str_coin("1", INJ_2, Decimals::Eighteen), + ], + ); + + let inj_to_swap = "1"; + let to_output_quantity = human_to_dec("8", Decimals::Six); + + let mut query_result: SwapEstimationResult = wasm + .query( + &contr_addr, + &QueryMsg::GetInputQuantity { + to_quantity: to_output_quantity, + source_denom: INJ_2.to_string(), + target_denom: USDC.to_string(), + }, + ) + .unwrap(); + + let expected_input_quantity = human_to_dec("0.903", Decimals::Eighteen); + let max_diff = human_to_dec("0.001", Decimals::Eighteen); + + assert!( + are_fpdecimals_approximately_equal(expected_input_quantity, query_result.result_quantity, max_diff), + "incorrect swap result estimate returned by query. Expected: {} INJ, actual: {} INJ, max diff: {} INJ", + expected_input_quantity.scaled(Decimals::Eighteen.get_decimals().neg()), + query_result.result_quantity.scaled(Decimals::Eighteen.get_decimals().neg()), + max_diff.scaled(Decimals::Eighteen.get_decimals().neg()) + ); + + let mut expected_fees = vec![ + FPCoin { + amount: human_to_dec("0.013365", Decimals::Six), + denom: USDT.to_string(), + }, + FPCoin { + amount: human_to_dec("0.01332", Decimals::Six), + denom: USDC.to_string(), + }, + ]; + + // we don't care too much about decimal fraction of the fee + assert_fee_is_as_expected( + &mut query_result.expected_fees, + &mut expected_fees, + human_to_dec("0.1", Decimals::Six), + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 2, + "wrong number of denoms in contract balances" + ); + + wasm.execute( + &contr_addr, + &ExecuteMsg::SwapExactOutput { + target_denom: USDC.to_string(), + target_output_quantity: to_output_quantity, + }, + &[str_coin(inj_to_swap, INJ_2, Decimals::Eighteen)], + &swapper, + ) + .unwrap(); + + let from_balance = query_bank_balance(&bank, INJ_2, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, USDC, swapper.address().as_str()); + + let expected_inj_leftover = + human_to_dec(inj_to_swap, Decimals::Eighteen) - expected_input_quantity; + assert_eq!( + from_balance, expected_inj_leftover, + "incorrect original amount was left after swap" + ); + + let expected_amount = human_to_dec("8.00711", Decimals::Six); + + assert_eq!( + to_balance, + expected_amount, + "Swapper received less than expected minimum amount. Expected: {} USDC, actual: {} USDC", + expected_amount.scaled(Decimals::Six.get_decimals().neg()), + to_balance.scaled(Decimals::Six.get_decimals().neg()), + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 2, + "wrong number of denoms in contract balances" + ); + + // let's check contract's USDT balance + let contract_usdt_balance_before = + FPDecimal::must_from_str(contract_balances_before[0].amount.as_str()); + let contract_usdt_balance_after = + FPDecimal::must_from_str(contract_balances_after[0].amount.as_str()); + + assert!( + contract_usdt_balance_after >= contract_usdt_balance_before, + "Contract lost some money after swap. Actual balance: {} USDT, previous balance: {} USDT", + contract_usdt_balance_after, + contract_usdt_balance_before + ); + + // contract is allowed to earn extra 0.001 USDT from the swap of ~$8 worth of INJ + let max_diff = human_to_dec("0.001", Decimals::Six); + + assert!( + are_fpdecimals_approximately_equal( + contract_usdt_balance_after, + contract_usdt_balance_before, + max_diff, + ), + "Contract balance changed too much. Actual balance: {} USDT, previous balance: {} USDT. Max diff: {} USDT", + contract_usdt_balance_after.scaled(Decimals::Six.get_decimals().neg()), + contract_usdt_balance_before.scaled(Decimals::Six.get_decimals().neg()), + max_diff.scaled(Decimals::Six.get_decimals().neg()) + ); + + // let's check contract's USDC balance + let contract_usdc_balance_before = + FPDecimal::must_from_str(contract_balances_before[1].amount.as_str()); + let contract_usdc_balance_after = + FPDecimal::must_from_str(contract_balances_after[1].amount.as_str()); + + assert!( + contract_usdc_balance_after >= contract_usdc_balance_before, + "Contract lost some money after swap. Actual balance: {} USDC, previous balance: {} USDC", + contract_usdc_balance_after, + contract_usdc_balance_before + ); + + // contract is allowed to earn extra 0.001 USDC from the swap of ~$8 worth of INJ + let max_diff = human_to_dec("0.001", Decimals::Six); + + assert!( + are_fpdecimals_approximately_equal( + contract_usdc_balance_after, + contract_usdc_balance_before, + max_diff, + ), + "Contract balance changed too much. Actual balance: {} USDC, previous balance: {} USDC. Max diff: {} USDC", + contract_usdc_balance_after.scaled(Decimals::Six.get_decimals().neg()), + contract_usdc_balance_before.scaled(Decimals::Six.get_decimals().neg()), + max_diff.scaled(Decimals::Six.get_decimals().neg()) + ); +} + +#[test] +fn it_doesnt_lose_buffer_if_exact_swap_of_eth_to_atom_is_executed_multiple_times() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = must_init_account_with_funds(&app, &[str_coin("1", INJ, Decimals::Eighteen)]); + + let _validator = app + .get_first_validator_signing_account(INJ.to_string(), 1.2f64) + .unwrap(); + + let owner = must_init_account_with_funds( + &app, + &[ + str_coin("1", ETH, Decimals::Eighteen), + str_coin("1", ATOM, Decimals::Six), + str_coin("1_000", USDT, Decimals::Six), + str_coin("10_000", INJ, Decimals::Eighteen), + ], + ); + + let spot_market_1_id = launch_realistic_weth_usdt_spot_market(&exchange, &owner); + let spot_market_2_id = launch_realistic_atom_usdt_spot_market(&exchange, &owner); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("1_000", USDT, Decimals::Six)], + ); + + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + let eth_to_swap = "4.08"; + let iterations = 100i128; + + let swapper = must_init_account_with_funds( + &app, + &[ + str_coin( + (FPDecimal::must_from_str(eth_to_swap) * FPDecimal::from(iterations)) + .to_string() + .as_str(), + ETH, + Decimals::Eighteen, + ), + str_coin("1", INJ, Decimals::Eighteen), + ], + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let mut counter = 0; + + while counter < iterations { + create_realistic_eth_usdt_buy_orders_from_spreadsheet( + &app, + &spot_market_1_id, + &trader1, + &trader2, + ); + create_realistic_atom_usdt_sell_orders_from_spreadsheet( + &app, + &spot_market_2_id, + &trader1, + &trader2, + &trader3, + ); + + app.increase_time(1); + + wasm.execute( + &contr_addr, + &ExecuteMsg::SwapExactOutput { + target_denom: ATOM.to_string(), + target_output_quantity: human_to_dec("906", Decimals::Six), + }, + &[str_coin(eth_to_swap, ETH, Decimals::Eighteen)], + &swapper, + ) + .unwrap(); + + counter += 1 + } + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let contract_balance_usdt_after = + FPDecimal::must_from_str(contract_balances_after[0].amount.as_str()); + let contract_balance_usdt_before = + FPDecimal::must_from_str(contract_balances_before[0].amount.as_str()); + + assert!( + contract_balance_usdt_after >= contract_balance_usdt_before, + "Contract lost some money after swap. Starting balance: {contract_balance_usdt_after}, Current balance: {contract_balance_usdt_before}", + ); + + // single swap with the same values results in < 0.7 USDT earning, so we expected that 100 same swaps + // won't change balance by more than 0.7 * 100 = 70 USDT + let max_diff = human_to_dec("0.7", Decimals::Six) * FPDecimal::from(iterations); + + assert!(are_fpdecimals_approximately_equal( + contract_balance_usdt_after, + contract_balance_usdt_before, + max_diff, + ), "Contract balance changed too much. Starting balance: {}, Current balance: {}. Max diff: {}", + contract_balance_usdt_before.scaled(Decimals::Six.get_decimals().neg()), + contract_balance_usdt_after.scaled(Decimals::Six.get_decimals().neg()), + max_diff.scaled(Decimals::Six.get_decimals().neg()) + ); +} + +#[test] +fn it_reverts_when_funds_provided_are_below_required_to_get_exact_amount() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = must_init_account_with_funds(&app, &[str_coin("1", INJ, Decimals::Eighteen)]); + + let _validator = app + .get_first_validator_signing_account(INJ.to_string(), 1.2f64) + .unwrap(); + let owner = must_init_account_with_funds( + &app, + &[ + str_coin("1", ETH, Decimals::Eighteen), + str_coin("1", ATOM, Decimals::Six), + str_coin("1_000", USDT, Decimals::Six), + str_coin("10_000", INJ, Decimals::Eighteen), + str_coin("10_000", INJ_2, Decimals::Eighteen), + ], + ); + + let spot_market_1_id = launch_realistic_inj_usdt_spot_market(&exchange, &owner); + let spot_market_2_id = launch_realistic_atom_usdt_spot_market(&exchange, &owner); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("1_000", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + INJ_2, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + create_realistic_inj_usdt_buy_orders_from_spreadsheet( + &app, + &spot_market_1_id, + &trader1, + &trader2, + ); + create_realistic_atom_usdt_sell_orders_from_spreadsheet( + &app, + &spot_market_2_id, + &trader1, + &trader2, + &trader3, + ); + + app.increase_time(1); + + let inj_to_swap = "608"; + + let swapper = must_init_account_with_funds( + &app, + &[ + str_coin(inj_to_swap, INJ_2, Decimals::Eighteen), + str_coin("1", INJ, Decimals::Eighteen), + ], + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let exact_quantity_to_receive = human_to_dec("600", Decimals::Six); + let swapper_inj_balance_before = query_bank_balance(&bank, INJ_2, swapper.address().as_str()); + + let _: SwapEstimationResult = wasm + .query( + &contr_addr, + &QueryMsg::GetInputQuantity { + source_denom: INJ_2.to_string(), + target_denom: ATOM.to_string(), + to_quantity: exact_quantity_to_receive, + }, + ) + .unwrap(); + + let execute_result = wasm + .execute( + &contr_addr, + &ExecuteMsg::SwapExactOutput { + target_denom: ATOM.to_string(), + target_output_quantity: exact_quantity_to_receive, + }, + &[str_coin(inj_to_swap, INJ_2, Decimals::Eighteen)], + &swapper, + ) + .unwrap_err(); + + assert!(execute_result.to_string().contains("Provided amount of 608000000000000000000 is below required amount of 609714000000000000000"), "wrong error message"); + + let swapper_inj_balance_after = query_bank_balance(&bank, INJ_2, swapper.address().as_str()); + let swapper_atom_balance_after = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + + assert_eq!( + swapper_inj_balance_before, swapper_inj_balance_after, + "some amount of INJ was exchanged" + ); + + assert_eq!( + FPDecimal::ZERO, + swapper_atom_balance_after, + "swapper received some ATOM" + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let contract_usdt_balance_before = + FPDecimal::must_from_str(contract_balances_before[0].amount.as_str()); + let contract_usdt_balance_after = + FPDecimal::must_from_str(contract_balances_after[0].amount.as_str()); + + assert_eq!( + contract_usdt_balance_after, contract_usdt_balance_before, + "Contract's balance changed after failed swap", + ); +} + +// TEST TEMPLATES + +// source much more expensive than target +fn exact_two_hop_eth_atom_swap_test_template( + exact_quantity_to_receive: FPDecimal, + max_diff_percentage: Percent, +) { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = must_init_account_with_funds(&app, &[str_coin("1", INJ, Decimals::Eighteen)]); + + let _validator = app + .get_first_validator_signing_account(INJ.to_string(), 1.2f64) + .unwrap(); + let owner = must_init_account_with_funds( + &app, + &[ + str_coin("1", ETH, Decimals::Eighteen), + str_coin("1", ATOM, Decimals::Six), + str_coin("1_000", USDT, Decimals::Six), + str_coin("10_000", INJ, Decimals::Eighteen), + ], + ); + + let spot_market_1_id = launch_realistic_weth_usdt_spot_market(&exchange, &owner); + let spot_market_2_id = launch_realistic_atom_usdt_spot_market(&exchange, &owner); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("1_000", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + create_realistic_eth_usdt_buy_orders_from_spreadsheet( + &app, + &spot_market_1_id, + &trader1, + &trader2, + ); + create_realistic_atom_usdt_sell_orders_from_spreadsheet( + &app, + &spot_market_2_id, + &trader1, + &trader2, + &trader3, + ); + + app.increase_time(1); + + let eth_to_swap = "4.08"; + + let swapper = must_init_account_with_funds( + &app, + &[ + str_coin(eth_to_swap, ETH, Decimals::Eighteen), + str_coin("1", INJ, Decimals::Eighteen), + ], + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let query_result: SwapEstimationResult = wasm + .query( + &contr_addr, + &QueryMsg::GetInputQuantity { + source_denom: ETH.to_string(), + target_denom: ATOM.to_string(), + to_quantity: exact_quantity_to_receive, + }, + ) + .unwrap(); + + wasm.execute( + &contr_addr, + &ExecuteMsg::SwapExactOutput { + target_denom: ATOM.to_string(), + target_output_quantity: exact_quantity_to_receive, + }, + &[str_coin(eth_to_swap, ETH, Decimals::Eighteen)], + &swapper, + ) + .unwrap(); + + let expected_difference = + human_to_dec(eth_to_swap, Decimals::Eighteen) - query_result.result_quantity; + let swapper_eth_balance_after = query_bank_balance(&bank, ETH, swapper.address().as_str()); + let swapper_atom_balance_after = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + + assert_eq!( + swapper_eth_balance_after, expected_difference, + "wrong amount of ETH was exchanged" + ); + + let one_percent_diff = exact_quantity_to_receive + * (FPDecimal::must_from_str(max_diff_percentage.0) / FPDecimal::from(100u128)); + + assert!( + swapper_atom_balance_after >= exact_quantity_to_receive, + "swapper got less than exact amount required -> expected: {} ATOM, actual: {} ATOM", + exact_quantity_to_receive.scaled(Decimals::Six.get_decimals().neg()), + swapper_atom_balance_after.scaled(Decimals::Six.get_decimals().neg()) + ); + + assert!( + are_fpdecimals_approximately_equal( + swapper_atom_balance_after, + exact_quantity_to_receive, + one_percent_diff, + ), + "swapper did not receive expected exact amount +/- {}% -> expected: {} ATOM, actual: {} ATOM, max diff: {} ATOM", + max_diff_percentage.0, + exact_quantity_to_receive.scaled(Decimals::Six.get_decimals().neg()), + swapper_atom_balance_after.scaled(Decimals::Six.get_decimals().neg()), + one_percent_diff.scaled(Decimals::Six.get_decimals().neg()) + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let contract_usdt_balance_before = + FPDecimal::must_from_str(contract_balances_before[0].amount.as_str()); + let contract_usdt_balance_after = + FPDecimal::must_from_str(contract_balances_after[0].amount.as_str()); + + assert!( + contract_usdt_balance_after >= contract_usdt_balance_before, + "Contract lost some money after swap. Actual balance: {contract_usdt_balance_after}, previous balance: {contract_usdt_balance_before}", + ); + + // contract is allowed to earn extra 0.7 USDT from the swap of ~$8150 worth of ETH + let max_diff = human_to_dec("0.7", Decimals::Six); + + assert!( + are_fpdecimals_approximately_equal( + contract_usdt_balance_after, + contract_usdt_balance_before, + max_diff, + ), + "Contract balance changed too much. Actual balance: {} USDT, previous balance: {} USDT. Max diff: {} USDT", + contract_usdt_balance_after.scaled(Decimals::Six.get_decimals().neg()), + contract_usdt_balance_before.scaled(Decimals::Six.get_decimals().neg()), + max_diff.scaled(Decimals::Six.get_decimals().neg()) + ); +} + +// source more or less similarly priced as target +fn exact_two_hop_inj_atom_swap_test_template( + exact_quantity_to_receive: FPDecimal, + max_diff_percentage: Percent, +) { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = must_init_account_with_funds(&app, &[str_coin("1", INJ, Decimals::Eighteen)]); + + let _validator = app + .get_first_validator_signing_account(INJ.to_string(), 1.2f64) + .unwrap(); + let owner = must_init_account_with_funds( + &app, + &[ + str_coin("1", ETH, Decimals::Eighteen), + str_coin("1", ATOM, Decimals::Six), + str_coin("1_000", USDT, Decimals::Six), + str_coin("10_000", INJ, Decimals::Eighteen), + str_coin("10_000", INJ_2, Decimals::Eighteen), + ], + ); + + let spot_market_1_id = launch_realistic_inj_usdt_spot_market(&exchange, &owner); + let spot_market_2_id = launch_realistic_atom_usdt_spot_market(&exchange, &owner); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("1_000", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + INJ_2, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + create_realistic_inj_usdt_buy_orders_from_spreadsheet( + &app, + &spot_market_1_id, + &trader1, + &trader2, + ); + create_realistic_atom_usdt_sell_orders_from_spreadsheet( + &app, + &spot_market_2_id, + &trader1, + &trader2, + &trader3, + ); + + app.increase_time(1); + + let inj_to_swap = "973.258"; + + let swapper = must_init_account_with_funds( + &app, + &[ + str_coin(inj_to_swap, INJ_2, Decimals::Eighteen), + str_coin("1", INJ, Decimals::Eighteen), + ], + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let query_result: SwapEstimationResult = wasm + .query( + &contr_addr, + &QueryMsg::GetInputQuantity { + source_denom: INJ_2.to_string(), + target_denom: ATOM.to_string(), + to_quantity: exact_quantity_to_receive, + }, + ) + .unwrap(); + + wasm.execute( + &contr_addr, + &ExecuteMsg::SwapExactOutput { + target_denom: ATOM.to_string(), + target_output_quantity: exact_quantity_to_receive, + }, + &[str_coin(inj_to_swap, INJ_2, Decimals::Eighteen)], + &swapper, + ) + .unwrap(); + + let expected_difference = + human_to_dec(inj_to_swap, Decimals::Eighteen) - query_result.result_quantity; + let swapper_inj_balance_after = query_bank_balance(&bank, INJ_2, swapper.address().as_str()); + let swapper_atom_balance_after = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + + assert_eq!( + swapper_inj_balance_after, expected_difference, + "wrong amount of INJ was exchanged" + ); + + assert!( + swapper_atom_balance_after >= exact_quantity_to_receive, + "swapper got less than exact amount required -> expected: {} ATOM, actual: {} ATOM", + exact_quantity_to_receive.scaled(Decimals::Six.get_decimals().neg()), + swapper_atom_balance_after.scaled(Decimals::Six.get_decimals().neg()) + ); + + let one_percent_diff = exact_quantity_to_receive + * (FPDecimal::must_from_str(max_diff_percentage.0) / FPDecimal::from(100u128)); + + assert!( + are_fpdecimals_approximately_equal( + swapper_atom_balance_after, + exact_quantity_to_receive, + one_percent_diff, + ), + "swapper did not receive expected exact ATOM amount +/- {}% -> expected: {} ATOM, actual: {} ATOM, max diff: {} ATOM", + max_diff_percentage.0, + exact_quantity_to_receive.scaled(Decimals::Six.get_decimals().neg()), + swapper_atom_balance_after.scaled(Decimals::Six.get_decimals().neg()), + one_percent_diff.scaled(Decimals::Six.get_decimals().neg()) + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let contract_usdt_balance_before = + FPDecimal::must_from_str(contract_balances_before[0].amount.as_str()); + let contract_usdt_balance_after = + FPDecimal::must_from_str(contract_balances_after[0].amount.as_str()); + + assert!( + contract_usdt_balance_after >= contract_usdt_balance_before, + "Contract lost some money after swap. Actual balance: {contract_usdt_balance_after}, previous balance: {contract_usdt_balance_before}", + ); + + // contract is allowed to earn extra 0.7 USDT from the swap of ~$8150 worth of INJ + let max_diff = human_to_dec("0.7", Decimals::Six); + + assert!( + are_fpdecimals_approximately_equal( + contract_usdt_balance_after, + contract_usdt_balance_before, + max_diff, + ), + "Contract balance changed too much. Actual balance: {} USDT, previous balance: {} USDT. Max diff: {} USDT", + contract_usdt_balance_after.scaled(Decimals::Six.get_decimals().neg()), + contract_usdt_balance_before.scaled(Decimals::Six.get_decimals().neg()), + max_diff.scaled(Decimals::Six.get_decimals().neg()) + ); +} + +// source much cheaper than target +fn exact_two_hop_inj_eth_swap_test_template( + exact_quantity_to_receive: FPDecimal, + max_diff_percentage: Percent, +) { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = must_init_account_with_funds(&app, &[str_coin("1", INJ, Decimals::Eighteen)]); + + let _validator = app + .get_first_validator_signing_account(INJ.to_string(), 1.2f64) + .unwrap(); + let owner = must_init_account_with_funds( + &app, + &[ + str_coin("1", ETH, Decimals::Eighteen), + str_coin("1_000", USDT, Decimals::Six), + str_coin("10_000", INJ, Decimals::Eighteen), + str_coin("10_000", INJ_2, Decimals::Eighteen), + ], + ); + + let spot_market_1_id = launch_realistic_inj_usdt_spot_market(&exchange, &owner); + let spot_market_2_id = launch_realistic_weth_usdt_spot_market(&exchange, &owner); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("1_000", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + INJ_2, + ETH, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + create_realistic_inj_usdt_buy_orders_from_spreadsheet( + &app, + &spot_market_1_id, + &trader1, + &trader2, + ); + create_realistic_eth_usdt_sell_orders_from_spreadsheet( + &app, + &spot_market_2_id, + &trader1, + &trader2, + &trader3, + ); + + app.increase_time(1); + + let inj_to_swap = "973.258"; + + let swapper = must_init_account_with_funds( + &app, + &[ + str_coin(inj_to_swap, INJ_2, Decimals::Eighteen), + str_coin("1", INJ, Decimals::Eighteen), + ], + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let query_result: SwapEstimationResult = wasm + .query( + &contr_addr, + &QueryMsg::GetInputQuantity { + source_denom: INJ_2.to_string(), + target_denom: ETH.to_string(), + to_quantity: exact_quantity_to_receive, + }, + ) + .unwrap(); + + wasm.execute( + &contr_addr, + &ExecuteMsg::SwapExactOutput { + target_denom: ETH.to_string(), + target_output_quantity: exact_quantity_to_receive, + }, + &[str_coin(inj_to_swap, INJ_2, Decimals::Eighteen)], + &swapper, + ) + .unwrap(); + + let expected_difference = + human_to_dec(inj_to_swap, Decimals::Eighteen) - query_result.result_quantity; + let swapper_inj_balance_after = query_bank_balance(&bank, INJ_2, swapper.address().as_str()); + let swapper_atom_balance_after = query_bank_balance(&bank, ETH, swapper.address().as_str()); + + assert_eq!( + swapper_inj_balance_after, expected_difference, + "wrong amount of INJ was exchanged" + ); + + assert!( + swapper_atom_balance_after >= exact_quantity_to_receive, + "swapper got less than exact amount required -> expected: {} ETH, actual: {} ETH", + exact_quantity_to_receive.scaled(Decimals::Eighteen.get_decimals().neg()), + swapper_atom_balance_after.scaled(Decimals::Eighteen.get_decimals().neg()) + ); + + let one_percent_diff = exact_quantity_to_receive + * (FPDecimal::must_from_str(max_diff_percentage.0) / FPDecimal::from(100u128)); + + assert!( + are_fpdecimals_approximately_equal( + swapper_atom_balance_after, + exact_quantity_to_receive, + one_percent_diff, + ), + "swapper did not receive expected exact ETH amount +/- {}% -> expected: {} ETH, actual: {} ETH, max diff: {} ETH", + max_diff_percentage.0, + exact_quantity_to_receive.scaled(Decimals::Eighteen.get_decimals().neg()), + swapper_atom_balance_after.scaled(Decimals::Eighteen.get_decimals().neg()), + one_percent_diff.scaled(Decimals::Eighteen.get_decimals().neg()) + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let contract_usdt_balance_before = + FPDecimal::must_from_str(contract_balances_before[0].amount.as_str()); + let contract_usdt_balance_after = + FPDecimal::must_from_str(contract_balances_after[0].amount.as_str()); + + assert!( + contract_usdt_balance_after >= contract_usdt_balance_before, + "Contract lost some money after swap. Actual balance: {contract_usdt_balance_after}, previous balance: {contract_usdt_balance_before}", + ); + + // contract is allowed to earn extra 0.7 USDT from the swap of ~$8500 worth of INJ + let max_diff = human_to_dec("0.82", Decimals::Six); + + assert!( + are_fpdecimals_approximately_equal( + contract_usdt_balance_after, + contract_usdt_balance_before, + max_diff, + ), + "Contract balance changed too much. Actual balance: {} USDT, previous balance: {} USDT. Max diff: {} USDT", + contract_usdt_balance_after.scaled(Decimals::Six.get_decimals().neg()), + contract_usdt_balance_before.scaled(Decimals::Six.get_decimals().neg()), + max_diff.scaled(Decimals::Six.get_decimals().neg()) + ); +} diff --git a/contracts/swap/src/testing/integration_tests/integration_realistic_tests_min_quantity.rs b/contracts/swap/src/testing/integration_tests/integration_realistic_tests_min_quantity.rs new file mode 100644 index 0000000..ed4d64a --- /dev/null +++ b/contracts/swap/src/testing/integration_tests/integration_realistic_tests_min_quantity.rs @@ -0,0 +1,1416 @@ +use injective_test_tube::{Account, Bank, Exchange, InjectiveTestApp, Module, RunnerResult, Wasm}; +use std::ops::Neg; + +use crate::helpers::Scaled; +use injective_math::FPDecimal; + +use crate::msg::{ExecuteMsg, QueryMsg}; +use crate::testing::test_utils::{ + are_fpdecimals_approximately_equal, assert_fee_is_as_expected, + create_realistic_atom_usdt_sell_orders_from_spreadsheet, + create_realistic_eth_usdt_buy_orders_from_spreadsheet, + create_realistic_eth_usdt_sell_orders_from_spreadsheet, + create_realistic_inj_usdt_buy_orders_from_spreadsheet, + create_realistic_usdt_usdc_both_side_orders, human_to_dec, init_rich_account, + init_self_relaying_contract_and_get_address, launch_realistic_atom_usdt_spot_market, + launch_realistic_inj_usdt_spot_market, launch_realistic_usdt_usdc_spot_market, + launch_realistic_weth_usdt_spot_market, must_init_account_with_funds, query_all_bank_balances, + query_bank_balance, set_route_and_assert_success, str_coin, Decimals, ATOM, + DEFAULT_ATOMIC_MULTIPLIER, DEFAULT_SELF_RELAYING_FEE_PART, DEFAULT_TAKER_FEE, ETH, INJ, INJ_2, + USDC, USDT, +}; +use crate::types::{FPCoin, SwapEstimationResult}; + +/* + This test suite focuses on using using realistic values both for spot markets and for orders and + focuses on swaps requesting minimum amount. + + ATOM/USDT market parameters were taken from mainnet. ETH/USDT market parameters mirror WETH/USDT + spot market on mainnet. INJ_2/USDT mirrors mainnet's INJ/USDT market (we used a different denom + to avoid mixing balance changes related to swap with ones related to gas payments). + + Hardcoded values used in these tests come from the second tab of this spreadsheet: + https://docs.google.com/spreadsheets/d/1-0epjX580nDO_P2mm1tSjhvjJVppsvrO1BC4_wsBeyA/edit?usp=sharing + + In all tests contract is configured to self-relay trades and thus receive a 60% fee discount. +*/ + +#[test] +fn happy_path_two_hops_swap_eth_atom_realistic_values_self_relaying() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = must_init_account_with_funds(&app, &[str_coin("1", INJ, Decimals::Eighteen)]); + + let _validator = app + .get_first_validator_signing_account(INJ.to_string(), 1.2f64) + .unwrap(); + let owner = must_init_account_with_funds( + &app, + &[ + str_coin("1", ETH, Decimals::Eighteen), + str_coin("1", ATOM, Decimals::Six), + str_coin("1_000", USDT, Decimals::Six), + str_coin("10_000", INJ, Decimals::Eighteen), + ], + ); + + let spot_market_1_id = launch_realistic_weth_usdt_spot_market(&exchange, &owner); + let spot_market_2_id = launch_realistic_atom_usdt_spot_market(&exchange, &owner); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("1_000", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + create_realistic_eth_usdt_buy_orders_from_spreadsheet( + &app, + &spot_market_1_id, + &trader1, + &trader2, + ); + create_realistic_atom_usdt_sell_orders_from_spreadsheet( + &app, + &spot_market_2_id, + &trader1, + &trader2, + &trader3, + ); + + app.increase_time(1); + + let eth_to_swap = "4.08"; + + let swapper = must_init_account_with_funds( + &app, + &[ + str_coin(eth_to_swap, ETH, Decimals::Eighteen), + str_coin("1", INJ, Decimals::Eighteen), + ], + ); + + let mut query_result: SwapEstimationResult = wasm + .query( + &contr_addr, + &QueryMsg::GetOutputQuantity { + source_denom: ETH.to_string(), + target_denom: ATOM.to_string(), + from_quantity: human_to_dec(eth_to_swap, Decimals::Eighteen), + }, + ) + .unwrap(); + + // it's expected that it is slightly less than what's in the spreadsheet + let expected_amount = human_to_dec("906.17", Decimals::Six); + + assert_eq!( + query_result.result_quantity, expected_amount, + "incorrect swap result estimate returned by query" + ); + + let mut expected_fees = vec![ + FPCoin { + amount: human_to_dec("12.221313", Decimals::Six), + denom: "usdt".to_string(), + }, + FPCoin { + amount: human_to_dec("12.184704", Decimals::Six), + denom: "usdt".to_string(), + }, + ]; + + // we don't care too much about decimal fraction of the fee + assert_fee_is_as_expected( + &mut query_result.expected_fees, + &mut expected_fees, + human_to_dec("0.1", Decimals::Six), + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + wasm.execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: ATOM.to_string(), + min_output_quantity: FPDecimal::from(906u128), + }, + &[str_coin(eth_to_swap, ETH, Decimals::Eighteen)], + &swapper, + ) + .unwrap(); + + let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + + assert_eq!( + from_balance, + FPDecimal::ZERO, + "some of the original amount wasn't swapped" + ); + + assert!( + to_balance >= expected_amount, + "Swapper received less than expected minimum amount. Expected: {} ATOM, actual: {} ATOM", + expected_amount.scaled(Decimals::Six.get_decimals().neg()), + to_balance.scaled(Decimals::Six.get_decimals().neg()), + ); + + let max_diff = human_to_dec("0.1", Decimals::Six); + + assert!( + are_fpdecimals_approximately_equal( + expected_amount, + to_balance, + max_diff, + ), + "Swapper did not receive expected amount. Expected: {} ATOM, actual: {} ATOM, max diff: {} ATOM", + expected_amount.scaled(Decimals::Six.get_decimals().neg()), + to_balance.scaled(Decimals::Six.get_decimals().neg()), + max_diff.scaled(Decimals::Six.get_decimals().neg()) + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let contract_usdt_balance_before = + FPDecimal::must_from_str(contract_balances_before[0].amount.as_str()); + let contract_usdt_balance_after = + FPDecimal::must_from_str(contract_balances_after[0].amount.as_str()); + + assert!( + contract_usdt_balance_after >= contract_usdt_balance_before, + "Contract lost some money after swap. Actual balance: {} USDT, previous balance: {} USDT", + contract_usdt_balance_after, + contract_usdt_balance_before + ); + + // contract is allowed to earn extra 0.7 USDT from the swap of ~$8150 worth of ETH + let max_diff = human_to_dec("0.7", Decimals::Six); + + assert!( + are_fpdecimals_approximately_equal( + contract_usdt_balance_after, + contract_usdt_balance_before, + max_diff, + ), + "Contract balance changed too much. Actual balance: {} USDT, previous balance: {} USDT. Max diff: {} USDT", + contract_usdt_balance_after.scaled(Decimals::Six.get_decimals().neg()), + contract_usdt_balance_before.scaled(Decimals::Six.get_decimals().neg()), + max_diff.scaled(Decimals::Six.get_decimals().neg()) + ); +} + +#[test] +fn happy_path_two_hops_swap_inj_eth_realistic_values_self_relaying() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = must_init_account_with_funds(&app, &[str_coin("1", INJ, Decimals::Eighteen)]); + + let _validator = app + .get_first_validator_signing_account(INJ.to_string(), 1.2f64) + .unwrap(); + let owner = must_init_account_with_funds( + &app, + &[ + str_coin("1", ETH, Decimals::Eighteen), + str_coin("1_000", USDT, Decimals::Six), + str_coin("10_000", INJ, Decimals::Eighteen), + str_coin("1", INJ_2, Decimals::Eighteen), + ], + ); + + let spot_market_1_id = launch_realistic_inj_usdt_spot_market(&exchange, &owner); + let spot_market_2_id = launch_realistic_weth_usdt_spot_market(&exchange, &owner); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("1_000", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + INJ_2, + ETH, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + create_realistic_inj_usdt_buy_orders_from_spreadsheet( + &app, + &spot_market_1_id, + &trader1, + &trader2, + ); + create_realistic_eth_usdt_sell_orders_from_spreadsheet( + &app, + &spot_market_2_id, + &trader1, + &trader2, + &trader3, + ); + + app.increase_time(1); + + let inj_to_swap = "973.258"; + + let swapper = must_init_account_with_funds( + &app, + &[ + str_coin(inj_to_swap, INJ_2, Decimals::Eighteen), + str_coin("1", INJ, Decimals::Eighteen), + ], + ); + + let mut query_result: SwapEstimationResult = wasm + .query( + &contr_addr, + &QueryMsg::GetOutputQuantity { + source_denom: INJ_2.to_string(), + target_denom: ETH.to_string(), + from_quantity: human_to_dec(inj_to_swap, Decimals::Eighteen), + }, + ) + .unwrap(); + + // it's expected that it is slightly less than what's in the spreadsheet + let expected_amount = human_to_dec("3.994", Decimals::Eighteen); + + assert_eq!( + query_result.result_quantity, expected_amount, + "incorrect swap result estimate returned by query" + ); + + let mut expected_fees = vec![ + FPCoin { + amount: human_to_dec("12.73828775", Decimals::Six), + denom: "usdt".to_string(), + }, + FPCoin { + amount: human_to_dec("12.70013012", Decimals::Six), + denom: "usdt".to_string(), + }, + ]; + + // we don't care too much about decimal fraction of the fee + assert_fee_is_as_expected( + &mut query_result.expected_fees, + &mut expected_fees, + human_to_dec("0.1", Decimals::Six), + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + wasm.execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: ETH.to_string(), + min_output_quantity: FPDecimal::from(906u128), + }, + &[str_coin(inj_to_swap, INJ_2, Decimals::Eighteen)], + &swapper, + ) + .unwrap(); + + let from_balance = query_bank_balance(&bank, INJ_2, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); + + assert_eq!( + from_balance, + FPDecimal::ZERO, + "some of the original amount wasn't swapped" + ); + + assert!( + to_balance >= expected_amount, + "Swapper received less than expected minimum amount. Expected: {} ETH, actual: {} ETH", + expected_amount.scaled(Decimals::Eighteen.get_decimals().neg()), + to_balance.scaled(Decimals::Eighteen.get_decimals().neg()), + ); + + let max_diff = human_to_dec("0.1", Decimals::Eighteen); + + assert!( + are_fpdecimals_approximately_equal( + expected_amount, + to_balance, + max_diff, + ), + "Swapper did not receive expected amount. Expected: {} ETH, actual: {} ETH, max diff: {} ETH", + expected_amount.scaled(Decimals::Eighteen.get_decimals().neg()), + to_balance.scaled(Decimals::Eighteen.get_decimals().neg()), + max_diff.scaled(Decimals::Eighteen.get_decimals().neg()) + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let contract_usdt_balance_before = + FPDecimal::must_from_str(contract_balances_before[0].amount.as_str()); + let contract_usdt_balance_after = + FPDecimal::must_from_str(contract_balances_after[0].amount.as_str()); + + assert!( + contract_usdt_balance_after >= contract_usdt_balance_before, + "Contract lost some money after swap. Actual balance: {} USDT, previous balance: {} USDT", + contract_usdt_balance_after, + contract_usdt_balance_before + ); + + // contract is allowed to earn extra 0.7 USDT from the swap of ~$8150 worth of ETH + let max_diff = human_to_dec("0.7", Decimals::Six); + + assert!( + are_fpdecimals_approximately_equal( + contract_usdt_balance_after, + contract_usdt_balance_before, + max_diff, + ), + "Contract balance changed too much. Actual balance: {} USDT, previous balance: {} USDT. Max diff: {} USDT", + contract_usdt_balance_after.scaled(Decimals::Six.get_decimals().neg()), + contract_usdt_balance_before.scaled(Decimals::Six.get_decimals().neg()), + max_diff.scaled(Decimals::Six.get_decimals().neg()) + ); +} + +#[test] +fn happy_path_two_hops_swap_inj_atom_realistic_values_self_relaying() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = must_init_account_with_funds(&app, &[str_coin("1", INJ, Decimals::Eighteen)]); + + let _validator = app + .get_first_validator_signing_account(INJ.to_string(), 1.2f64) + .unwrap(); + let owner = must_init_account_with_funds( + &app, + &[ + str_coin("1", ETH, Decimals::Eighteen), + str_coin("1", ATOM, Decimals::Six), + str_coin("1_000", USDT, Decimals::Six), + str_coin("10_000", INJ, Decimals::Eighteen), + str_coin("1", INJ_2, Decimals::Eighteen), + ], + ); + + let spot_market_1_id = launch_realistic_inj_usdt_spot_market(&exchange, &owner); + let spot_market_2_id = launch_realistic_atom_usdt_spot_market(&exchange, &owner); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("1_000", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + INJ_2, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + create_realistic_inj_usdt_buy_orders_from_spreadsheet( + &app, + &spot_market_1_id, + &trader1, + &trader2, + ); + create_realistic_atom_usdt_sell_orders_from_spreadsheet( + &app, + &spot_market_2_id, + &trader1, + &trader2, + &trader3, + ); + + app.increase_time(1); + + let inj_to_swap = "973.258"; + + let swapper = must_init_account_with_funds( + &app, + &[ + str_coin(inj_to_swap, INJ_2, Decimals::Eighteen), + str_coin("1", INJ, Decimals::Eighteen), + ], + ); + + let mut query_result: SwapEstimationResult = wasm + .query( + &contr_addr, + &QueryMsg::GetOutputQuantity { + source_denom: INJ_2.to_string(), + target_denom: ATOM.to_string(), + from_quantity: human_to_dec(inj_to_swap, Decimals::Eighteen), + }, + ) + .unwrap(); + + // it's expected that it is slightly less than what's in the spreadsheet + let expected_amount = human_to_dec("944.26", Decimals::Six); + + assert_eq!( + query_result.result_quantity, expected_amount, + "incorrect swap result estimate returned by query" + ); + + let mut expected_fees = vec![ + FPCoin { + amount: human_to_dec("12.73828775", Decimals::Six), + denom: "usdt".to_string(), + }, + FPCoin { + amount: human_to_dec("12.70013012", Decimals::Six), + denom: "usdt".to_string(), + }, + ]; + + // we don't care too much about decimal fraction of the fee + assert_fee_is_as_expected( + &mut query_result.expected_fees, + &mut expected_fees, + human_to_dec("0.1", Decimals::Six), + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + wasm.execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: ATOM.to_string(), + min_output_quantity: FPDecimal::from(944u128), + }, + &[str_coin(inj_to_swap, INJ_2, Decimals::Eighteen)], + &swapper, + ) + .unwrap(); + + let from_balance = query_bank_balance(&bank, INJ_2, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + + assert_eq!( + from_balance, + FPDecimal::ZERO, + "some of the original amount wasn't swapped" + ); + + assert!( + to_balance >= expected_amount, + "Swapper received less than expected minimum amount. Expected: {} ATOM, actual: {} ATOM", + expected_amount.scaled(Decimals::Six.get_decimals().neg()), + to_balance.scaled(Decimals::Six.get_decimals().neg()), + ); + + let max_diff = human_to_dec("0.1", Decimals::Six); + + assert!( + are_fpdecimals_approximately_equal( + expected_amount, + to_balance, + max_diff, + ), + "Swapper did not receive expected amount. Expected: {} ATOM, actual: {} ATOM, max diff: {} ATOM", + expected_amount.scaled(Decimals::Six.get_decimals().neg()), + to_balance.scaled(Decimals::Six.get_decimals().neg()), + max_diff.scaled(Decimals::Six.get_decimals().neg()) + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let contract_usdt_balance_before = + FPDecimal::must_from_str(contract_balances_before[0].amount.as_str()); + let contract_usdt_balance_after = + FPDecimal::must_from_str(contract_balances_after[0].amount.as_str()); + + assert!( + contract_usdt_balance_after >= contract_usdt_balance_before, + "Contract lost some money after swap. Actual balance: {} USDT, previous balance: {} USDT", + contract_usdt_balance_after.scaled(Decimals::Six.get_decimals().neg()), + contract_usdt_balance_before.scaled(Decimals::Six.get_decimals().neg()) + ); + + // contract is allowed to earn extra 0.82 USDT from the swap of ~$8500 worth of INJ + let max_diff = human_to_dec("0.82", Decimals::Six); + + assert!( + are_fpdecimals_approximately_equal( + contract_usdt_balance_after, + contract_usdt_balance_before, + max_diff, + ), + "Contract balance changed too much. Actual balance: {}, previous balance: {}. Max diff: {}", + contract_usdt_balance_after.scaled(Decimals::Six.get_decimals().neg()), + contract_usdt_balance_before.scaled(Decimals::Six.get_decimals().neg()), + max_diff.scaled(Decimals::Six.get_decimals().neg()) + ); +} + +#[test] +fn it_executes_swap_between_markets_using_different_quote_assets_self_relaying() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = must_init_account_with_funds(&app, &[str_coin("1", INJ, Decimals::Eighteen)]); + let _validator = app + .get_first_validator_signing_account(INJ.to_string(), 1.2f64) + .unwrap(); + + let owner = must_init_account_with_funds( + &app, + &[ + str_coin("1_000", USDT, Decimals::Six), + str_coin("1_000", USDC, Decimals::Six), + str_coin("10_000", INJ, Decimals::Eighteen), + str_coin("1", INJ_2, Decimals::Eighteen), + ], + ); + + let spot_market_1_id = launch_realistic_inj_usdt_spot_market(&exchange, &owner); + let spot_market_2_id = launch_realistic_usdt_usdc_spot_market(&exchange, &owner); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[ + str_coin("10", USDC, Decimals::Six), + str_coin("500", USDT, Decimals::Six), + ], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + INJ_2, + USDC, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + + create_realistic_inj_usdt_buy_orders_from_spreadsheet( + &app, + &spot_market_1_id, + &trader1, + &trader2, + ); + create_realistic_usdt_usdc_both_side_orders(&app, &spot_market_2_id, &trader1); + + app.increase_time(1); + + let swapper = must_init_account_with_funds( + &app, + &[ + str_coin("1", INJ, Decimals::Eighteen), + str_coin("1", INJ_2, Decimals::Eighteen), + ], + ); + + let inj_to_swap = "1"; + + let mut query_result: SwapEstimationResult = wasm + .query( + &contr_addr, + &QueryMsg::GetOutputQuantity { + source_denom: INJ_2.to_string(), + target_denom: USDC.to_string(), + from_quantity: human_to_dec(inj_to_swap, Decimals::Eighteen), + }, + ) + .unwrap(); + + let expected_amount = human_to_dec("8.867", Decimals::Six); + let max_diff = human_to_dec("0.001", Decimals::Six); + + assert!( + are_fpdecimals_approximately_equal(expected_amount, query_result.result_quantity, max_diff), + "incorrect swap result estimate returned by query" + ); + + let mut expected_fees = vec![ + FPCoin { + amount: human_to_dec("0.013365", Decimals::Six), + denom: USDT.to_string(), + }, + FPCoin { + amount: human_to_dec("0.01332", Decimals::Six), + denom: USDC.to_string(), + }, + ]; + + // we don't care too much about decimal fraction of the fee + assert_fee_is_as_expected( + &mut query_result.expected_fees, + &mut expected_fees, + human_to_dec("0.1", Decimals::Six), + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 2, + "wrong number of denoms in contract balances" + ); + + wasm.execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: USDC.to_string(), + min_output_quantity: FPDecimal::from(8u128), + }, + &[str_coin(inj_to_swap, INJ_2, Decimals::Eighteen)], + &swapper, + ) + .unwrap(); + + let from_balance = query_bank_balance(&bank, INJ_2, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, USDC, swapper.address().as_str()); + + assert_eq!( + from_balance, + FPDecimal::ZERO, + "some of the original amount wasn't swapped" + ); + + assert!( + to_balance >= expected_amount, + "Swapper received less than expected minimum amount. Expected: {} USDC, actual: {} USDC", + expected_amount.scaled(Decimals::Eighteen.get_decimals().neg()), + to_balance.scaled(Decimals::Eighteen.get_decimals().neg()), + ); + + let max_diff = human_to_dec("0.1", Decimals::Eighteen); + + assert!( + are_fpdecimals_approximately_equal( + expected_amount, + to_balance, + max_diff, + ), + "Swapper did not receive expected amount. Expected: {} USDC, actual: {} USDC, max diff: {} USDC", + expected_amount.scaled(Decimals::Eighteen.get_decimals().neg()), + to_balance.scaled(Decimals::Eighteen.get_decimals().neg()), + max_diff.scaled(Decimals::Eighteen.get_decimals().neg()) + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 2, + "wrong number of denoms in contract balances" + ); + + // let's check contract's USDT balance + let contract_usdt_balance_before = + FPDecimal::must_from_str(contract_balances_before[0].amount.as_str()); + let contract_usdt_balance_after = + FPDecimal::must_from_str(contract_balances_after[0].amount.as_str()); + + assert!( + contract_usdt_balance_after >= contract_usdt_balance_before, + "Contract lost some money after swap. Actual balance: {} USDT, previous balance: {} USDT", + contract_usdt_balance_after, + contract_usdt_balance_before + ); + + // contract is allowed to earn extra 0.001 USDT from the swap of ~$8 worth of INJ + let max_diff = human_to_dec("0.001", Decimals::Six); + + assert!( + are_fpdecimals_approximately_equal( + contract_usdt_balance_after, + contract_usdt_balance_before, + max_diff, + ), + "Contract balance changed too much. Actual balance: {} USDT, previous balance: {} USDT. Max diff: {} USDT", + contract_usdt_balance_after.scaled(Decimals::Six.get_decimals().neg()), + contract_usdt_balance_before.scaled(Decimals::Six.get_decimals().neg()), + max_diff.scaled(Decimals::Six.get_decimals().neg()) + ); + + // let's check contract's USDC balance + let contract_usdc_balance_before = + FPDecimal::must_from_str(contract_balances_before[1].amount.as_str()); + let contract_usdc_balance_after = + FPDecimal::must_from_str(contract_balances_after[1].amount.as_str()); + + assert!( + contract_usdc_balance_after >= contract_usdc_balance_before, + "Contract lost some money after swap. Actual balance: {} USDC, previous balance: {} USDC", + contract_usdc_balance_after, + contract_usdc_balance_before + ); + + // contract is allowed to earn extra 0.001 USDC from the swap of ~$8 worth of INJ + let max_diff = human_to_dec("0.001", Decimals::Six); + + assert!( + are_fpdecimals_approximately_equal( + contract_usdc_balance_after, + contract_usdc_balance_before, + max_diff, + ), + "Contract balance changed too much. Actual balance: {} USDC, previous balance: {} USDC. Max diff: {} USDC", + contract_usdc_balance_after.scaled(Decimals::Six.get_decimals().neg()), + contract_usdc_balance_before.scaled(Decimals::Six.get_decimals().neg()), + max_diff.scaled(Decimals::Six.get_decimals().neg()) + ); +} + +#[test] +fn it_doesnt_lose_buffer_if_executed_multiple_times() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = must_init_account_with_funds(&app, &[str_coin("1", INJ, Decimals::Eighteen)]); + + let _validator = app + .get_first_validator_signing_account(INJ.to_string(), 1.2f64) + .unwrap(); + + let owner = must_init_account_with_funds( + &app, + &[ + str_coin("1", ETH, Decimals::Eighteen), + str_coin("1", ATOM, Decimals::Six), + str_coin("1_000", USDT, Decimals::Six), + str_coin("10_000", INJ, Decimals::Eighteen), + ], + ); + + let spot_market_1_id = launch_realistic_weth_usdt_spot_market(&exchange, &owner); + let spot_market_2_id = launch_realistic_atom_usdt_spot_market(&exchange, &owner); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("1_000", USDT, Decimals::Six)], + ); + + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + let eth_to_swap = "4.08"; + + let swapper = must_init_account_with_funds( + &app, + &[ + str_coin( + (FPDecimal::must_from_str(eth_to_swap) * FPDecimal::from(100u128)) + .to_string() + .as_str(), + ETH, + Decimals::Eighteen, + ), + str_coin("1", INJ, Decimals::Eighteen), + ], + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let mut counter = 0; + let iterations = 100; + + while counter < iterations { + create_realistic_eth_usdt_buy_orders_from_spreadsheet( + &app, + &spot_market_1_id, + &trader1, + &trader2, + ); + create_realistic_atom_usdt_sell_orders_from_spreadsheet( + &app, + &spot_market_2_id, + &trader1, + &trader2, + &trader3, + ); + + app.increase_time(1); + + wasm.execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: ATOM.to_string(), + min_output_quantity: FPDecimal::from(906u128), + }, + &[str_coin(eth_to_swap, ETH, Decimals::Eighteen)], + &swapper, + ) + .unwrap(); + + counter += 1 + } + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let contract_balance_usdt_after = + FPDecimal::must_from_str(contract_balances_after[0].amount.as_str()); + let contract_balance_usdt_before = + FPDecimal::must_from_str(contract_balances_before[0].amount.as_str()); + + assert!( + contract_balance_usdt_after >= contract_balance_usdt_before, + "Contract lost some money after swap. Starting balance: {}, Current balance: {}", + contract_balance_usdt_after, + contract_balance_usdt_before + ); + + // single swap with the same values results in < 0.7 USDT earning, so we expected that 100 same swaps + // won't change balance by more than 0.7 * 100 = 70 USDT + let max_diff = human_to_dec("0.7", Decimals::Six) * FPDecimal::from(iterations as u128); + + assert!(are_fpdecimals_approximately_equal( + contract_balance_usdt_after, + contract_balance_usdt_before, + max_diff, + ), "Contract balance changed too much. Starting balance: {}, Current balance: {}. Max diff: {}", + contract_balance_usdt_before.scaled(Decimals::Six.get_decimals().neg()), + contract_balance_usdt_after.scaled(Decimals::Six.get_decimals().neg()), + max_diff.scaled(Decimals::Six.get_decimals().neg()) + ); +} + +/* + This test shows that query overestimates the amount of USDT needed to execute the swap. It seems + that in reality we get a better price when selling ETH than the one returned by query and can + execute the swap with less USDT. + + It's easiest to check by commenting out the query_result assert and running the test. It will + pass and amounts will perfectly match our assertions. +*/ +#[ignore] +#[test] +fn it_correctly_calculates_required_funds_when_querying_buy_with_minimum_buffer_and_realistic_values( +) { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = must_init_account_with_funds(&app, &[str_coin("1", INJ, Decimals::Eighteen)]); + + let _validator = app + .get_first_validator_signing_account(INJ.to_string(), 1.2f64) + .unwrap(); + let owner = must_init_account_with_funds( + &app, + &[ + str_coin("1", ETH, Decimals::Eighteen), + str_coin("1", ATOM, Decimals::Six), + str_coin("1_000", USDT, Decimals::Six), + str_coin("10_000", INJ, Decimals::Eighteen), + ], + ); + + let spot_market_1_id = launch_realistic_weth_usdt_spot_market(&exchange, &owner); + let spot_market_2_id = launch_realistic_atom_usdt_spot_market(&exchange, &owner); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("51", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + create_realistic_eth_usdt_buy_orders_from_spreadsheet( + &app, + &spot_market_1_id, + &trader1, + &trader2, + ); + create_realistic_atom_usdt_sell_orders_from_spreadsheet( + &app, + &spot_market_2_id, + &trader1, + &trader2, + &trader3, + ); + + app.increase_time(1); + + let eth_to_swap = "4.08"; + + let swapper = must_init_account_with_funds( + &app, + &[ + str_coin(eth_to_swap, ETH, Decimals::Eighteen), + str_coin("1", INJ, Decimals::Eighteen), + ], + ); + + let query_result: FPDecimal = wasm + .query( + &contr_addr, + &QueryMsg::GetOutputQuantity { + source_denom: ETH.to_string(), + target_denom: ATOM.to_string(), + from_quantity: human_to_dec(eth_to_swap, Decimals::Eighteen), + }, + ) + .unwrap(); + + assert_eq!( + query_result, + human_to_dec("906.195", Decimals::Six), + "incorrect swap result estimate returned by query" + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + wasm.execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: ATOM.to_string(), + min_output_quantity: FPDecimal::from(906u128), + }, + &[str_coin(eth_to_swap, ETH, Decimals::Eighteen)], + &swapper, + ) + .unwrap(); + + let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + assert_eq!( + from_balance, + FPDecimal::ZERO, + "some of the original amount wasn't swapped" + ); + assert_eq!( + to_balance, + human_to_dec("906.195", Decimals::Six), + "swapper did not receive expected amount" + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let atom_amount_below_min_tick_size = FPDecimal::must_from_str("0.0005463"); + let mut dust_value = atom_amount_below_min_tick_size * human_to_dec("8.89", Decimals::Six); + + let fee_refund = dust_value + * FPDecimal::must_from_str(&format!( + "{}", + DEFAULT_TAKER_FEE * DEFAULT_ATOMIC_MULTIPLIER * DEFAULT_SELF_RELAYING_FEE_PART + )); + + dust_value += fee_refund; + + let expected_contract_usdt_balance = + FPDecimal::must_from_str(contract_balances_before[0].amount.as_str()) + dust_value; + let actual_contract_balance = + FPDecimal::must_from_str(contract_balances_after[0].amount.as_str()); + let contract_balance_diff = expected_contract_usdt_balance - actual_contract_balance; + + // here the actual difference is 0.000067 USDT, which we attribute differences between decimal precision of Rust/Go and Google Sheets + assert!( + human_to_dec("0.0001", Decimals::Six) - contract_balance_diff > FPDecimal::ZERO, + "contract balance has changed too much after swap" + ); +} + +/* + This test shows that in some edge cases we calculate required funds differently than the chain does. + When estimating balance hold for atomic market order chain doesn't take into account whether sender is + also fee recipient, while we do. This leads to a situation where we estimate required funds to be + lower than what's expected by the chain, which makes the swap fail. + + In this test we skip query estimation and go straight to executing swap. +*/ +#[ignore] +#[test] +fn it_correctly_calculates_required_funds_when_executing_buy_with_minimum_buffer_and_realistic_values( +) { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = must_init_account_with_funds(&app, &[str_coin("1", INJ, Decimals::Eighteen)]); + + let _validator = app + .get_first_validator_signing_account(INJ.to_string(), 1.2f64) + .unwrap(); + let owner = must_init_account_with_funds( + &app, + &[ + str_coin("1", ETH, Decimals::Eighteen), + str_coin("1", ATOM, Decimals::Six), + str_coin("1_000", USDT, Decimals::Six), + str_coin("10_000", INJ, Decimals::Eighteen), + ], + ); + + let spot_market_1_id = launch_realistic_weth_usdt_spot_market(&exchange, &owner); + let spot_market_2_id = launch_realistic_atom_usdt_spot_market(&exchange, &owner); + + // in reality we need to add at least 49 USDT to the buffer, even if according to contract's calculations 42 USDT would be enough to execute the swap + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("42", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + create_realistic_eth_usdt_buy_orders_from_spreadsheet( + &app, + &spot_market_1_id, + &trader1, + &trader2, + ); + create_realistic_atom_usdt_sell_orders_from_spreadsheet( + &app, + &spot_market_2_id, + &trader1, + &trader2, + &trader3, + ); + + app.increase_time(1); + + let eth_to_swap = "4.08"; + + let swapper = must_init_account_with_funds( + &app, + &[ + str_coin(eth_to_swap, ETH, Decimals::Eighteen), + str_coin("0.01", INJ, Decimals::Eighteen), + ], + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + wasm.execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: ATOM.to_string(), + min_output_quantity: FPDecimal::from(906u128), + }, + &[str_coin(eth_to_swap, ETH, Decimals::Eighteen)], + &swapper, + ) + .unwrap(); + + let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + assert_eq!( + from_balance, + FPDecimal::ZERO, + "some of the original amount wasn't swapped" + ); + assert_eq!( + to_balance, + human_to_dec("906.195", Decimals::Six), + "swapper did not receive expected amount" + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let contract_usdt_balance_before = + FPDecimal::must_from_str(contract_balances_before[0].amount.as_str()); + let contract_usdt_balance_after = + FPDecimal::must_from_str(contract_balances_after[0].amount.as_str()); + + assert!( + contract_usdt_balance_after >= contract_usdt_balance_before, + "Contract lost some money after swap. Actual balance: {}, previous balance: {}", + contract_usdt_balance_after, + contract_usdt_balance_before + ); + + // contract can earn max of 0.7 USDT, when exchanging ETH worth ~$8150 + let max_diff = human_to_dec("0.7", Decimals::Six); + + assert!( + are_fpdecimals_approximately_equal( + contract_usdt_balance_after, + contract_usdt_balance_before, + max_diff, + ), + "Contract balance changed too much. Actual balance: {}, previous balance: {}. Max diff: {}", + contract_usdt_balance_after.scaled(Decimals::Six.get_decimals().neg()), + contract_usdt_balance_before.scaled(Decimals::Six.get_decimals().neg()), + max_diff.scaled(Decimals::Six.get_decimals().neg()) + ); +} + +#[test] +fn it_returns_all_funds_if_there_is_not_enough_buffer_realistic_values() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + let _signer = must_init_account_with_funds(&app, &[str_coin("1", INJ, Decimals::Eighteen)]); + + let _validator = app + .get_first_validator_signing_account(INJ.to_string(), 1.2f64) + .unwrap(); + let owner = must_init_account_with_funds( + &app, + &[ + str_coin("1", ETH, Decimals::Eighteen), + str_coin("1", ATOM, Decimals::Six), + str_coin("1_000", USDT, Decimals::Six), + str_coin("10_000", INJ, Decimals::Eighteen), + ], + ); + + let spot_market_1_id = launch_realistic_weth_usdt_spot_market(&exchange, &owner); + let spot_market_2_id = launch_realistic_atom_usdt_spot_market(&exchange, &owner); + + // 41 USDT is just below the amount required to buy required ATOM amount + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("41", USDT, Decimals::Six)], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + ETH, + ATOM, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + let trader2 = init_rich_account(&app); + let trader3 = init_rich_account(&app); + + create_realistic_eth_usdt_buy_orders_from_spreadsheet( + &app, + &spot_market_1_id, + &trader1, + &trader2, + ); + create_realistic_atom_usdt_sell_orders_from_spreadsheet( + &app, + &spot_market_2_id, + &trader1, + &trader2, + &trader3, + ); + + app.increase_time(1); + + let eth_to_swap = "4.08"; + + let swapper = must_init_account_with_funds( + &app, + &[ + str_coin(eth_to_swap, ETH, Decimals::Eighteen), + str_coin("1", INJ, Decimals::Eighteen), + ], + ); + + let query_result: RunnerResult = wasm.query( + &contr_addr, + &QueryMsg::GetOutputQuantity { + source_denom: ETH.to_string(), + target_denom: ATOM.to_string(), + from_quantity: human_to_dec(eth_to_swap, Decimals::Eighteen), + }, + ); + + assert!(query_result.is_err(), "query should fail"); + + assert!( + query_result + .unwrap_err() + .to_string() + .contains("Swap amount too high"), + "incorrect error message in query result" + ); + + let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); + assert_eq!( + contract_balances_before.len(), + 1, + "wrong number of denoms in contract balances" + ); + + let execute_result = wasm.execute( + &contr_addr, + &ExecuteMsg::SwapMinOutput { + target_denom: ATOM.to_string(), + min_output_quantity: FPDecimal::from(906u128), + }, + &[str_coin(eth_to_swap, ETH, Decimals::Eighteen)], + &swapper, + ); + + assert!(execute_result.is_err(), "execute should fail"); + + let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); + let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); + + assert_eq!( + from_balance, + human_to_dec(eth_to_swap, Decimals::Eighteen), + "source balance changed after failed swap" + ); + assert_eq!( + to_balance, + FPDecimal::ZERO, + "target balance changed after failed swap" + ); + + let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); + assert_eq!( + contract_balances_after.len(), + 1, + "wrong number of denoms in contract balances" + ); + assert_eq!( + contract_balances_before[0].amount, contract_balances_after[0].amount, + "contract balance has changed after failed swap" + ); +} diff --git a/contracts/swap/src/testing/integration_tests/mod.rs b/contracts/swap/src/testing/integration_tests/mod.rs new file mode 100644 index 0000000..516ec20 --- /dev/null +++ b/contracts/swap/src/testing/integration_tests/mod.rs @@ -0,0 +1,3 @@ +mod integration_logic_tests; +mod integration_realistic_tests_exact_quantity; +mod integration_realistic_tests_min_quantity;