From a694d8fa983d1734dc26283b202cd661b14f328f Mon Sep 17 00:00:00 2001 From: maxrobot Date: Wed, 30 Oct 2024 21:35:59 +0000 Subject: [PATCH] chore: fix integration tests --- Cargo.lock | 245 +-- Cargo.toml | 3 +- contracts/swap/Cargo.toml | 3 +- contracts/swap/src/swap.rs | 2 +- contracts/swap/src/testing/authz_tests.rs | 50 +- contracts/swap/src/testing/config_tests.rs | 23 +- .../src/testing/integration_logic_tests.rs | 1593 +++++++++++++++ ...egration_realistic_tests_exact_quantity.rs | 1795 +++++++++++++++++ ...ntegration_realistic_tests_min_quantity.rs | 1425 +++++++++++++ contracts/swap/src/testing/mod.rs | 7 +- contracts/swap/src/testing/queries_tests.rs | 132 +- contracts/swap/src/testing/test_utils.rs | 136 +- contracts/swap/src/types.rs | 2 - 13 files changed, 4990 insertions(+), 426 deletions(-) create mode 100644 contracts/swap/src/testing/integration_logic_tests.rs create mode 100644 contracts/swap/src/testing/integration_realistic_tests_exact_quantity.rs create mode 100644 contracts/swap/src/testing/integration_realistic_tests_min_quantity.rs diff --git a/Cargo.lock b/Cargo.lock index 7d41228..0e021d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -471,20 +471,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32560304ab4c365791fd307282f76637213d8083c1a98490c35159cd67852237" dependencies = [ "prost 0.12.6", - "prost-types", + "prost-types 0.12.6", "tendermint-proto 0.34.1", ] -[[package]] -name = "cosmos-sdk-proto" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8ce7f4797cdf5cd18be6555ff3f0a8d37023c2d60f3b2708895d601b85c1c46" -dependencies = [ - "prost 0.13.3", - "tendermint-proto 0.39.1", -] - [[package]] name = "cosmrs" version = "0.15.0" @@ -492,28 +482,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47126f5364df9387b9d8559dcef62e99010e1d4098f39eb3f7ee4b5c254e40ea" dependencies = [ "bip32", - "cosmos-sdk-proto 0.20.0", - "ecdsa", - "eyre", - "k256", - "rand_core 0.6.4", - "serde 1.0.214", - "serde_json 1.0.132", - "signature", - "subtle-encoding", - "tendermint 0.34.1", - "tendermint-rpc 0.34.1", - "thiserror", -] - -[[package]] -name = "cosmrs" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f90935b72d9fa65a2a784e09f25778637b7e88e9d6f87c717081470f7fa726" -dependencies = [ - "bip32", - "cosmos-sdk-proto 0.25.0", + "cosmos-sdk-proto", "ecdsa", "eyre", "k256", @@ -522,8 +491,8 @@ dependencies = [ "serde_json 1.0.132", "signature", "subtle-encoding", - "tendermint 0.39.1", - "tendermint-rpc 0.39.1", + "tendermint", + "tendermint-rpc", "thiserror", ] @@ -1493,9 +1462,9 @@ dependencies = [ [[package]] name = "injective-cosmwasm" -version = "0.3.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a551fbe7bae0747a41ce81a1e7d5ba96ef089a7f0b3f05ab5a9b510248a709a7" +checksum = "23e93e9438844b10add3eb40ed1e8c92689824ac080d207f856412a73551c221" dependencies = [ "cosmwasm-std", "cw-storage-plus", @@ -1533,7 +1502,7 @@ dependencies = [ "cosmwasm-std", "injective-std-derive", "prost 0.12.6", - "prost-types", + "prost-types 0.12.6", "schemars", "serde 1.0.214", "serde-cw-value", @@ -1560,7 +1529,7 @@ checksum = "2a45747c74fca8aedafd94df74c6b9edf091c586ead96957e3c17e96abd6228b" dependencies = [ "base64 0.21.7", "bindgen", - "cosmrs 0.15.0", + "cosmrs", "cosmwasm-std", "hex", "injective-cosmwasm", @@ -2033,6 +2002,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +dependencies = [ + "bytes", + "prost-derive 0.11.9", +] + [[package]] name = "prost" version = "0.12.6" @@ -2053,6 +2032,19 @@ dependencies = [ "prost-derive 0.13.3", ] +[[package]] +name = "prost-derive" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +dependencies = [ + "anyhow", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "prost-derive" version = "0.12.6" @@ -2079,6 +2071,15 @@ dependencies = [ "syn 2.0.85", ] +[[package]] +name = "prost-types" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" +dependencies = [ + "prost 0.11.9", +] + [[package]] name = "prost-types" version = "0.12.6" @@ -2626,15 +2627,6 @@ dependencies = [ "syn 2.0.85", ] -[[package]] -name = "serde_spanned" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" -dependencies = [ - "serde 1.0.214", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2779,6 +2771,7 @@ dependencies = [ "schemars", "serde 1.0.214", "serde-json-wasm", + "test-tube-inj", "thiserror", ] @@ -2847,7 +2840,7 @@ dependencies = [ "num-traits", "once_cell", "prost 0.12.6", - "prost-types", + "prost-types 0.12.6", "ripemd", "serde 1.0.214", "serde_bytes", @@ -2862,36 +2855,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "tendermint" -version = "0.39.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3afea7809ffaaf1e5d9c3c9997cb3a834df7e94fbfab2fad2bc4577f1cde41" -dependencies = [ - "bytes", - "digest 0.10.7", - "ed25519", - "ed25519-consensus", - "flex-error", - "futures", - "k256", - "num-traits", - "once_cell", - "prost 0.13.3", - "ripemd", - "serde 1.0.214", - "serde_bytes", - "serde_json 1.0.132", - "serde_repr", - "sha2 0.10.8", - "signature", - "subtle", - "subtle-encoding", - "tendermint-proto 0.39.1", - "time", - "zeroize", -] - [[package]] name = "tendermint-config" version = "0.34.1" @@ -2901,37 +2864,23 @@ dependencies = [ "flex-error", "serde 1.0.214", "serde_json 1.0.132", - "tendermint 0.34.1", - "toml 0.5.11", - "url", -] - -[[package]] -name = "tendermint-config" -version = "0.39.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8add7b85b0282e5901521f78fe441956ac1e2752452f4e1f2c0ce7e1f10d485" -dependencies = [ - "flex-error", - "serde 1.0.214", - "serde_json 1.0.132", - "tendermint 0.39.1", - "toml 0.8.19", + "tendermint", + "toml", "url", ] [[package]] name = "tendermint-proto" -version = "0.34.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b797dd3d2beaaee91d2f065e7bdf239dc8d80bba4a183a288bc1279dd5a69a1e" +checksum = "c0cec054567d16d85e8c3f6a3139963d1a66d9d3051ed545d31562550e9bcc3d" dependencies = [ "bytes", "flex-error", "num-derive", "num-traits", - "prost 0.12.6", - "prost-types", + "prost 0.11.9", + "prost-types 0.11.9", "serde 1.0.214", "serde_bytes", "subtle-encoding", @@ -2940,13 +2889,16 @@ dependencies = [ [[package]] name = "tendermint-proto" -version = "0.39.1" +version = "0.34.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3abf34ecf33125621519e9952688e7a59a98232d51538037ba21fbe526a802" +checksum = "b797dd3d2beaaee91d2f065e7bdf239dc8d80bba4a183a288bc1279dd5a69a1e" dependencies = [ "bytes", "flex-error", - "prost 0.13.3", + "num-derive", + "num-traits", + "prost 0.12.6", + "prost-types 0.12.6", "serde 1.0.214", "serde_bytes", "subtle-encoding", @@ -2974,8 +2926,8 @@ dependencies = [ "serde_json 1.0.132", "subtle", "subtle-encoding", - "tendermint 0.34.1", - "tendermint-config 0.34.1", + "tendermint", + "tendermint-config", "tendermint-proto 0.34.1", "thiserror", "time", @@ -2986,39 +2938,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "tendermint-rpc" -version = "0.39.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9693f42544bf3b41be3cbbfa418650c86e137fb8f5a57981659a84b677721ecf" -dependencies = [ - "async-trait", - "bytes", - "flex-error", - "futures", - "getrandom", - "peg", - "pin-project", - "rand 0.8.5", - "reqwest", - "semver", - "serde 1.0.214", - "serde_bytes", - "serde_json 1.0.132", - "subtle", - "subtle-encoding", - "tendermint 0.39.1", - "tendermint-config 0.39.1", - "tendermint-proto 0.39.1", - "thiserror", - "time", - "tokio", - "tracing", - "url", - "uuid", - "walkdir", -] - [[package]] name = "termcolor" version = "1.4.1" @@ -3030,16 +2949,17 @@ dependencies = [ [[package]] name = "test-tube-inj" -version = "2.0.2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662c9081865602de48ca4c7cd2dbd6a1645060eea46614eb789c68f20c039707" +checksum = "3c3a4e34619e6417613fab682de9a196848f4a56653ac71b95b5860b6d87c7cd" dependencies = [ "base64 0.21.7", - "cosmrs 0.20.0", + "cosmrs", "cosmwasm-std", - "prost 0.13.3", + "prost 0.12.6", "serde 1.0.214", "serde_json 1.0.132", + "tendermint-proto 0.32.2", "thiserror", ] @@ -3182,40 +3102,6 @@ dependencies = [ "serde 1.0.214", ] -[[package]] -name = "toml" -version = "0.8.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" -dependencies = [ - "serde 1.0.214", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" -dependencies = [ - "serde 1.0.214", -] - -[[package]] -name = "toml_edit" -version = "0.22.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" -dependencies = [ - "indexmap 2.6.0", - "serde 1.0.214", - "serde_spanned", - "toml_datetime", - "winnow", -] - [[package]] name = "tower-service" version = "0.3.3" @@ -3626,15 +3512,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "0.6.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" -dependencies = [ - "memchr", -] - [[package]] name = "winreg" version = "0.50.0" diff --git a/Cargo.toml b/Cargo.toml index 62797ce..363f194 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ cosmwasm-std = { version = "2.1.0", features = [ "abort", "cosmwasm_1_2", cw-storage-plus = { version = "2.0.0" } cw-utils = { version = "2.0.0" } cw2 = { version = "2.0.0" } -injective-cosmwasm = { version = "0.3.0" } +injective-cosmwasm = { version = "=0.3.0" } injective-math = { version = "0.3.0" } injective-std = { version = "1.13.0" } injective-test-tube = { version = "1.13.2" } @@ -21,6 +21,7 @@ schemars = { version = "0.8.16", features = [ "enumset" ] } serde = { version = "1.0.193", default-features = false, features = [ "derive" ] } serde-json-wasm = { version = "1.0.1" } serde_json = { version = "1.0.120" } +test-tube-inj = { version = "=2.0.1" } thiserror = { version = "1.0.52" } [profile.release] diff --git a/contracts/swap/Cargo.toml b/contracts/swap/Cargo.toml index 08fb7ab..2cf9965 100644 --- a/contracts/swap/Cargo.toml +++ b/contracts/swap/Cargo.toml @@ -27,7 +27,6 @@ cw2 = { workspace = true } injective-cosmwasm = { workspace = true } injective-math = { workspace = true } injective-std = { workspace = true } -injective-testing = { workspace = true } prost = { workspace = true } schemars = { workspace = true } serde = { workspace = true } @@ -37,3 +36,5 @@ thiserror = { workspace = true } [dev-dependencies] injective-std = { workspace = true } injective-test-tube = { workspace = true } +injective-testing = { workspace = true } +test-tube-inj = { workspace = true } diff --git a/contracts/swap/src/swap.rs b/contracts/swap/src/swap.rs index 3809f4e..afff9a4 100644 --- a/contracts/swap/src/swap.rs +++ b/contracts/swap/src/swap.rs @@ -7,7 +7,7 @@ use crate::{ types::{CurrentSwapOperation, CurrentSwapStep, FPCoin, SwapEstimationAmount, SwapQuantityMode, SwapResults}, }; -use cosmwasm_std::{BankMsg, Coin, DepsMut, Env, Event, MessageInfo, Reply, Response, StdResult, SubMsg, Uint128}; +use cosmwasm_std::{BankMsg, Coin, DepsMut, Env, Event, MessageInfo, Reply, Response, StdResult, SubMsg}; use injective_cosmwasm::{ create_spot_market_order_msg, get_default_subaccount_id_for_checked_address, InjectiveMsgWrapper, InjectiveQuerier, InjectiveQueryWrapper, OrderType, SpotOrder, diff --git a/contracts/swap/src/testing/authz_tests.rs b/contracts/swap/src/testing/authz_tests.rs index 6c7b8c4..680a57c 100644 --- a/contracts/swap/src/testing/authz_tests.rs +++ b/contracts/swap/src/testing/authz_tests.rs @@ -2,16 +2,18 @@ use crate::{ msg::ExecuteMsg, testing::test_utils::{ create_contract_authorization, create_realistic_atom_usdt_sell_orders_from_spreadsheet, - create_realistic_eth_usdt_buy_orders_from_spreadsheet, init_rich_account, - init_self_relaying_contract_and_get_address, launch_realistic_atom_usdt_spot_market, - launch_realistic_weth_usdt_spot_market, must_init_account_with_funds, str_coin, Decimals, - ATOM, ETH, INJ, USDT, + create_realistic_eth_usdt_buy_orders_from_spreadsheet, init_rich_account, init_self_relaying_contract_and_get_address, + launch_realistic_atom_usdt_spot_market, launch_realistic_weth_usdt_spot_market, must_init_account_with_funds, str_coin, Decimals, ATOM, ETH, + INJ, USDT, }, }; -use cosmos_sdk_proto::{cosmwasm::wasm::v1::MsgExecuteContract, traits::MessageExt}; -use injective_std::{shim::Any, types::cosmos::authz::v1beta1::MsgExec}; +use injective_std::{ + shim::Any, + types::{cosmos::authz::v1beta1::MsgExec, cosmwasm::wasm::v1::MsgExecuteContract}, +}; use injective_test_tube::{Account, Authz, Exchange, InjectiveTestApp, Module, Wasm}; +use prost::Message; #[test] pub fn set_route_for_third_party_test() { @@ -33,11 +35,7 @@ pub fn set_route_for_third_party_test() { 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)], - ); + let contr_addr = init_self_relaying_contract_and_get_address(&wasm, &owner, &[str_coin("1_000", USDT, Decimals::Six)]); let trader1 = init_rich_account(&app); let trader2 = init_rich_account(&app); @@ -53,29 +51,15 @@ pub fn set_route_for_third_party_test() { None, ); - 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, - ); + 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 set_route_msg = ExecuteMsg::SetRoute { source_denom: ETH.to_string(), target_denom: ATOM.to_string(), - route: vec![ - spot_market_1_id.as_str().into(), - spot_market_2_id.as_str().into(), - ], + route: vec![spot_market_1_id.as_str().into(), spot_market_2_id.as_str().into()], }; let execute_msg = MsgExecuteContract { @@ -85,12 +69,11 @@ pub fn set_route_for_third_party_test() { funds: vec![], }; - // execute on more time to excercise account sequence let msg = MsgExec { grantee: trader1.address().to_string(), msgs: vec![Any { type_url: "/cosmwasm.wasm.v1.MsgExecuteContract".to_string(), - value: execute_msg.to_bytes().unwrap(), + value: execute_msg.encode_to_vec(), }], }; @@ -101,13 +84,10 @@ pub fn set_route_for_third_party_test() { grantee: trader1.address().to_string(), msgs: vec![Any { type_url: "/cosmwasm.wasm.v1.MsgExecuteContract".to_string(), - value: execute_msg.to_bytes().unwrap(), + value: execute_msg.encode_to_vec(), }], }; let err = authz.exec(msg, &trader1).unwrap_err(); - assert!( - err.to_string().contains("failed to update grant with key"), - "incorrect error returned by execute" - ); + assert!(err.to_string().contains("failed to get grant with given granter")); } diff --git a/contracts/swap/src/testing/config_tests.rs b/contracts/swap/src/testing/config_tests.rs index d61ce23..68ed7cc 100644 --- a/contracts/swap/src/testing/config_tests.rs +++ b/contracts/swap/src/testing/config_tests.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::testing::{mock_env, mock_info}; +use cosmwasm_std::testing::{message_info, mock_env}; use cosmwasm_std::{coins, Addr}; use injective_cosmwasm::{inj_mock_deps, OwnedDepsExt}; @@ -17,14 +17,12 @@ pub fn admin_can_update_config() { fee_recipient: Addr::unchecked(TEST_CONTRACT_ADDR), admin: Addr::unchecked(TEST_USER_ADDR), }; - CONFIG - .save(deps.as_mut_deps().storage, &config) - .expect("could not save config"); + CONFIG.save(deps.as_mut_deps().storage, &config).expect("could not save config"); let new_admin = Addr::unchecked("new_admin"); let new_fee_recipient = Addr::unchecked("new_fee_recipient"); - let info = mock_info(TEST_USER_ADDR, &coins(12, "eth")); + let info = message_info(&Addr::unchecked(TEST_USER_ADDR), &coins(12, "eth")); let msg = ExecuteMsg::UpdateConfig { admin: Some(new_admin.clone()), @@ -36,10 +34,7 @@ pub fn admin_can_update_config() { let config = CONFIG.load(deps.as_mut_deps().storage).unwrap(); assert_eq!(config.admin, new_admin, "admin was not updated"); - assert_eq!( - config.fee_recipient, new_fee_recipient, - "fee_recipient was not updated" - ); + assert_eq!(config.fee_recipient, new_fee_recipient, "fee_recipient was not updated"); res.events .iter() @@ -47,7 +42,7 @@ pub fn admin_can_update_config() { .expect("update_config event expected") .attributes .iter() - .find(|a| a.key == "admin" && a.value == new_admin) + .find(|a| a.key == "admin" && a.value == new_admin.to_string()) .expect("admin attribute expected"); res.events @@ -56,7 +51,7 @@ pub fn admin_can_update_config() { .expect("update_config event expected") .attributes .iter() - .find(|a| a.key == "fee_recipient" && a.value == new_fee_recipient) + .find(|a| a.key == "fee_recipient" && a.value == new_fee_recipient.to_string()) .expect("fee_recipient attribute expected"); } @@ -68,14 +63,12 @@ pub fn non_admin_cannot_update_config() { fee_recipient: Addr::unchecked(TEST_CONTRACT_ADDR), admin: Addr::unchecked(TEST_USER_ADDR), }; - CONFIG - .save(deps.as_mut_deps().storage, &config) - .expect("could not save config"); + CONFIG.save(deps.as_mut_deps().storage, &config).expect("could not save config"); let new_admin = Addr::unchecked("new_admin"); let new_fee_recipient = Addr::unchecked("new_fee_recipient"); - let info = mock_info("non_admin", &coins(12, "eth")); + let info = message_info(&Addr::unchecked("non_admin"), &coins(12, "eth")); let msg = ExecuteMsg::UpdateConfig { admin: Some(new_admin), diff --git a/contracts/swap/src/testing/integration_logic_tests.rs b/contracts/swap/src/testing/integration_logic_tests.rs new file mode 100644 index 0000000..2c76100 --- /dev/null +++ b/contracts/swap/src/testing/integration_logic_tests.rs @@ -0,0 +1,1593 @@ +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_realistic_atom_usdt_spot_market, launch_realistic_usdt_usdc_spot_market, + launch_realistic_weth_usdt_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}; +use cosmwasm_std::{coin, Addr}; +use injective_math::{round_to_min_tick, FPDecimal}; +use injective_test_tube::RunnerError::{ExecuteError, QueryError}; +use injective_test_tube::{Account, Bank, Exchange, InjectiveTestApp, Module, RunnerError, RunnerResult, SigningAccount, Wasm}; +use injective_testing::test_tube::exchange::launch_spot_market; + +/* + 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_custom(&exchange, &owner, "".to_string(), ETH, USDT); + // let spot_market_2_id = launch_spot_market_custom(&exchange, &owner, ATOM, USDT); + + 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("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_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("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_realistic_weth_usdt_spot_market(&exchange, &owner); + let spot_market_2_id = launch_realistic_atom_usdt_spot_market(&exchange, &owner); + let spot_market_3_id = launch_realistic_usdt_usdc_spot_market(&exchange, &owner); + + 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_realistic_weth_usdt_spot_market(&exchange, &owner); + let spot_market_2_id = launch_realistic_atom_usdt_spot_market(&exchange, &owner); + let spot_market_3_id = launch_realistic_usdt_usdc_spot_market(&exchange, &owner); + + 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_realistic_weth_usdt_spot_market(&exchange, &owner); + + 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_realistic_weth_usdt_spot_market(&exchange, &owner); + + 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_realistic_weth_usdt_spot_market(&exchange, &owner); + let spot_market_2_id = launch_realistic_atom_usdt_spot_market(&exchange, &owner); + + 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_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("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_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("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_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("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_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("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_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("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_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("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_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("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_realistic_weth_usdt_spot_market(&exchange, &owner); + let spot_market_2_id = launch_realistic_atom_usdt_spot_market(&exchange, &owner); + + 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_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("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_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_realistic_tests_exact_quantity.rs b/contracts/swap/src/testing/integration_realistic_tests_exact_quantity.rs new file mode 100644 index 0000000..c3c3f7b --- /dev/null +++ b/contracts/swap/src/testing/integration_realistic_tests_exact_quantity.rs @@ -0,0 +1,1795 @@ +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_ninja_inj_both_side_orders, 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_inj_usdt_sell_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_ninja_inj_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, NINJA, + 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_correctly_swaps_between_markets_using_different_quote_assets_self_relaying_ninja() { + 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("1_000", NINJA, Decimals::Six), + str_coin("10_000", INJ, Decimals::Eighteen), + str_coin("101", INJ_2, Decimals::Eighteen), + ], + ); + + let spot_market_1_id = launch_realistic_inj_usdt_spot_market(&exchange, &owner); + let spot_market_2_id = launch_realistic_ninja_inj_spot_market(&exchange, &owner); + + let contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[ + str_coin("100", INJ_2, Decimals::Eighteen), + str_coin("10", USDC, Decimals::Six), + str_coin("500", USDT, Decimals::Six), + ], + ); + set_route_and_assert_success( + &wasm, + &owner, + &contr_addr, + USDT, + NINJA, + vec![ + spot_market_1_id.as_str().into(), + spot_market_2_id.as_str().into(), + ], + ); + + let trader1 = init_rich_account(&app); + + create_realistic_inj_usdt_sell_orders_from_spreadsheet(&app, &spot_market_1_id, &trader1); + create_ninja_inj_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("100000", USDT, Decimals::Six), + ], + ); + + let usdt_to_swap = "100000"; + let to_output_quantity = human_to_dec("501000", Decimals::Six); + + let from_balance_before = query_bank_balance(&bank, USDT, swapper.address().as_str()); + let to_balance_before = query_bank_balance(&bank, NINJA, swapper.address().as_str()); + + wasm.execute( + &contr_addr, + &ExecuteMsg::SwapExactOutput { + target_denom: NINJA.to_string(), + target_output_quantity: to_output_quantity, + }, + &[str_coin(usdt_to_swap, USDT, Decimals::Six)], + &swapper, + ) + .unwrap(); + + let from_balance_after = query_bank_balance(&bank, USDT, swapper.address().as_str()); + let to_balance_after = query_bank_balance(&bank, NINJA, swapper.address().as_str()); + + // from 100000 USDT -> 96201.062128 USDT = 3798.937872 USDT + let expected_from_balance_before = human_to_dec("100000", Decimals::Six); + let expected_from_balance_after = human_to_dec("96201.062128", Decimals::Six); + + // from 0 NINJA to 501000 NINJA + let expected_to_balance_before = human_to_dec("0", Decimals::Six); + let expected_to_balance_after = human_to_dec("501000", Decimals::Six); + + assert_eq!( + from_balance_before, expected_from_balance_before, + "incorrect original amount was left after swap" + ); + assert_eq!( + to_balance_before, expected_to_balance_before, + "incorrect target amount after swap" + ); + assert_eq!( + from_balance_after, expected_from_balance_after, + "incorrect original amount was left after swap" + ); + assert_eq!( + to_balance_after, expected_to_balance_after, + "incorrect target amount after swap" + ); +} + +#[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_realistic_tests_min_quantity.rs b/contracts/swap/src/testing/integration_realistic_tests_min_quantity.rs new file mode 100644 index 0000000..6213738 --- /dev/null +++ b/contracts/swap/src/testing/integration_realistic_tests_min_quantity.rs @@ -0,0 +1,1425 @@ +use injective_test_tube::{ + Account, Bank, Exchange, InjectiveTestApp, Module, RunnerResult, SigningAccount, 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. +*/ + +pub fn happy_path_two_hops_test(app: InjectiveTestApp, owner: SigningAccount, contr_addr: String) { + let wasm = Wasm::new(&app); + let exchange = Exchange::new(&app); + let bank = Bank::new(&app); + + 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); + + 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_eth_atom_realistic_values_self_relaying() { + let app = InjectiveTestApp::new(); + let wasm = Wasm::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 contr_addr = init_self_relaying_contract_and_get_address( + &wasm, + &owner, + &[str_coin("1_000", USDT, Decimals::Six)], + ); + + happy_path_two_hops_test(app, owner, contr_addr); +} + +#[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/mod.rs b/contracts/swap/src/testing/mod.rs index 7b66f13..0b0eead 100644 --- a/contracts/swap/src/testing/mod.rs +++ b/contracts/swap/src/testing/mod.rs @@ -1,7 +1,10 @@ mod authz_tests; mod config_tests; -// mod migration_test; +// // mod migration_test; +mod integration_logic_tests; +mod integration_realistic_tests_exact_quantity; +mod integration_realistic_tests_min_quantity; mod queries_tests; mod storage_tests; mod swap_tests; -// pub mod test_utils; +pub mod test_utils; diff --git a/contracts/swap/src/testing/queries_tests.rs b/contracts/swap/src/testing/queries_tests.rs index 0363acc..5e714f5 100644 --- a/contracts/swap/src/testing/queries_tests.rs +++ b/contracts/swap/src/testing/queries_tests.rs @@ -14,8 +14,8 @@ use crate::msg::{FeeRecipient, InstantiateMsg}; use crate::queries::{estimate_swap_result, SwapQuantity}; use crate::state::get_all_swap_routes; use crate::testing::test_utils::{ - are_fpdecimals_approximately_equal, human_to_dec, mock_deps_eth_inj, - mock_realistic_deps_eth_atom, Decimals, MultiplierQueryBehavior, TEST_USER_ADDR, + are_fpdecimals_approximately_equal, human_to_dec, mock_deps_eth_inj, mock_realistic_deps_eth_atom, Decimals, MultiplierQueryBehavior, + TEST_USER_ADDR, }; use crate::types::{FPCoin, SwapRoute}; @@ -84,22 +84,18 @@ fn test_calculate_swap_price_external_fee_recipient_from_source_quantity() { let max_diff = human_to_dec("0.00001", Decimals::Six); - assert!(are_fpdecimals_approximately_equal( - expected_fee_1.amount, - actual_swap_result.expected_fees[0].amount, - max_diff, - ), "Wrong amount of first trx fee received when using source quantity. Expected: {}, Actual: {}", + assert!( + are_fpdecimals_approximately_equal(expected_fee_1.amount, actual_swap_result.expected_fees[0].amount, max_diff,), + "Wrong amount of first trx fee received when using source quantity. Expected: {}, Actual: {}", expected_fee_1.amount, actual_swap_result.expected_fees[0].amount ); - assert!(are_fpdecimals_approximately_equal( + assert!( + are_fpdecimals_approximately_equal(expected_fee_2.amount, actual_swap_result.expected_fees[1].amount, max_diff,), + "Wrong amount of second trx fee received when using source quantity. Expected: {}, Actual: {}", expected_fee_2.amount, - actual_swap_result.expected_fees[1].amount, - max_diff, - ), "Wrong amount of second trx fee received when using source quantity. Expected: {}, Actual: {}", - expected_fee_2.amount, - actual_swap_result.expected_fees[1].amount + actual_swap_result.expected_fees[1].amount ); } @@ -162,22 +158,18 @@ fn test_calculate_swap_price_external_fee_recipient_from_target_quantity() { let max_diff = human_to_dec("0.00001", Decimals::Six); - assert!(are_fpdecimals_approximately_equal( + assert!( + are_fpdecimals_approximately_equal(expected_fee_1.amount, actual_swap_result.expected_fees[0].amount, max_diff,), + "Wrong amount of first trx fee received when using source quantity. Expected: {}, Actual: {}", expected_fee_1.amount, - actual_swap_result.expected_fees[0].amount, - max_diff, - ), "Wrong amount of first trx fee received when using source quantity. Expected: {}, Actual: {}", - expected_fee_1.amount, - actual_swap_result.expected_fees[0].amount + actual_swap_result.expected_fees[0].amount ); - assert!(are_fpdecimals_approximately_equal( + assert!( + are_fpdecimals_approximately_equal(expected_fee_2.amount, actual_swap_result.expected_fees[1].amount, max_diff,), + "Wrong amount of second trx fee received when using source quantity. Expected: {}, Actual: {}", expected_fee_2.amount, - actual_swap_result.expected_fees[1].amount, - max_diff, - ), "Wrong amount of second trx fee received when using source quantity. Expected: {}, Actual: {}", - expected_fee_2.amount, - actual_swap_result.expected_fees[1].amount + actual_swap_result.expected_fees[1].amount ); } @@ -221,11 +213,7 @@ fn test_calculate_swap_price_self_fee_recipient_from_source_quantity() { "Wrong amount of swap execution estimate received" ); // value rounded to min tick - assert_eq!( - actual_swap_result.expected_fees.len(), - 2, - "Wrong number of fee entries received" - ); + assert_eq!(actual_swap_result.expected_fees.len(), 2, "Wrong number of fee entries received"); // values from the spreadsheet let expected_fee_1 = FPCoin { @@ -241,22 +229,18 @@ fn test_calculate_swap_price_self_fee_recipient_from_source_quantity() { let max_diff = human_to_dec("0.00001", Decimals::Six); - assert!(are_fpdecimals_approximately_equal( + assert!( + are_fpdecimals_approximately_equal(expected_fee_1.amount, actual_swap_result.expected_fees[0].amount, max_diff,), + "Wrong amount of first trx fee received when using source quantity. Expected: {}, Actual: {}", expected_fee_1.amount, - actual_swap_result.expected_fees[0].amount, - max_diff, - ), "Wrong amount of first trx fee received when using source quantity. Expected: {}, Actual: {}", - expected_fee_1.amount, - actual_swap_result.expected_fees[0].amount + actual_swap_result.expected_fees[0].amount ); - assert!(are_fpdecimals_approximately_equal( + assert!( + are_fpdecimals_approximately_equal(expected_fee_2.amount, actual_swap_result.expected_fees[1].amount, max_diff,), + "Wrong amount of second trx fee received when using source quantity. Expected: {}, Actual: {}", expected_fee_2.amount, - actual_swap_result.expected_fees[1].amount, - max_diff, - ), "Wrong amount of second trx fee received when using source quantity. Expected: {}, Actual: {}", - expected_fee_2.amount, - actual_swap_result.expected_fees[1].amount + actual_swap_result.expected_fees[1].amount ); } @@ -320,22 +304,18 @@ fn test_calculate_swap_price_self_fee_recipient_from_target_quantity() { let max_diff = human_to_dec("0.00001", Decimals::Six); - assert!(are_fpdecimals_approximately_equal( + assert!( + are_fpdecimals_approximately_equal(expected_fee_1.amount, actual_swap_result.expected_fees[0].amount, max_diff,), + "Wrong amount of first trx fee received when using source quantity. Expected: {}, Actual: {}", expected_fee_1.amount, - actual_swap_result.expected_fees[0].amount, - max_diff, - ), "Wrong amount of first trx fee received when using source quantity. Expected: {}, Actual: {}", - expected_fee_1.amount, - actual_swap_result.expected_fees[0].amount + actual_swap_result.expected_fees[0].amount ); - assert!(are_fpdecimals_approximately_equal( + assert!( + are_fpdecimals_approximately_equal(expected_fee_2.amount, actual_swap_result.expected_fees[1].amount, max_diff,), + "Wrong amount of second trx fee received when using source quantity. Expected: {}, Actual: {}", expected_fee_2.amount, - actual_swap_result.expected_fees[1].amount, - max_diff, - ), "Wrong amount of second trx fee received when using source quantity. Expected: {}, Actual: {}", - expected_fee_2.amount, - actual_swap_result.expected_fees[1].amount + actual_swap_result.expected_fees[1].amount ); } @@ -399,11 +379,7 @@ fn test_calculate_estimate_when_selling_both_quantity_directions_simple() { let max_diff = human_to_dec("0.1", Decimals::Eighteen); assert!( - are_fpdecimals_approximately_equal( - expected_fee.amount, - input_swap_estimate.expected_fees[0].amount, - max_diff, - ), + are_fpdecimals_approximately_equal(expected_fee.amount, input_swap_estimate.expected_fees[0].amount, max_diff,), "Wrong amount of trx fee received when using source quantity. Expected: {}, Actual: {}", expected_fee.amount, input_swap_estimate.expected_fees[0].amount @@ -426,11 +402,7 @@ fn test_calculate_estimate_when_selling_both_quantity_directions_simple() { ); assert!( - are_fpdecimals_approximately_equal( - output_swap_estimate.result_quantity, - eth_input_amount, - max_diff - ), + are_fpdecimals_approximately_equal(output_swap_estimate.result_quantity, eth_input_amount, max_diff), "Wrong amount of swap execution estimate received when using target quantity" ); @@ -443,11 +415,7 @@ fn test_calculate_estimate_when_selling_both_quantity_directions_simple() { let max_diff = human_to_dec("0.1", Decimals::Six); assert!( - are_fpdecimals_approximately_equal( - expected_fee.amount, - input_swap_estimate.expected_fees[0].amount, - max_diff, - ), + are_fpdecimals_approximately_equal(expected_fee.amount, input_swap_estimate.expected_fees[0].amount, max_diff,), "Wrong amount of trx fee received when using source quantity. Expected: {}, Actual: {}", expected_fee.amount, input_swap_estimate.expected_fees[0].amount @@ -515,11 +483,7 @@ fn test_calculate_estimate_when_buying_both_quantity_directions_simple() { let mut max_diff = human_to_dec("0.00001", Decimals::Six); assert!( - are_fpdecimals_approximately_equal( - expected_fee.amount, - input_swap_estimate.expected_fees[0].amount, - max_diff, - ), + are_fpdecimals_approximately_equal(expected_fee.amount, input_swap_estimate.expected_fees[0].amount, max_diff,), "Wrong amount of trx fee received when using source quantity. Expected: {}, Actual: {}", expected_fee.amount, input_swap_estimate.expected_fees[0].amount @@ -538,20 +502,12 @@ fn test_calculate_estimate_when_buying_both_quantity_directions_simple() { max_diff = usdt_input_amount * FPDecimal::must_from_str("0.00025"); assert!( - are_fpdecimals_approximately_equal( - output_swap_estimate.result_quantity, - usdt_input_amount, - max_diff - ), + are_fpdecimals_approximately_equal(output_swap_estimate.result_quantity, usdt_input_amount, max_diff), "Wrong amount of swap execution estimate received when using target quantity" ); assert!( - are_fpdecimals_approximately_equal( - expected_fee.amount, - input_swap_estimate.expected_fees[0].amount, - max_diff, - ), + are_fpdecimals_approximately_equal(expected_fee.amount, input_swap_estimate.expected_fees[0].amount, max_diff,), "Wrong amount of trx fee received when using source quantity. Expected: {}, Actual: {}", expected_fee.amount, input_swap_estimate.expected_fees[0].amount @@ -577,10 +533,7 @@ fn get_all_queries_returns_empty_array_if_no_routes_are_set() { let all_routes_result = get_all_swap_routes(deps.as_ref().storage, None, None); assert!(all_routes_result.is_ok(), "Error getting all routes"); - assert!( - all_routes_result.unwrap().is_empty(), - "Routes should be empty" - ); + assert!(all_routes_result.unwrap().is_empty(), "Routes should be empty"); } #[test] @@ -653,4 +606,7 @@ fn get_all_queries_returns_expected_array_if_routes_are_set() { vec![eth_inj_route, eth_usdt_route, usdt_inj_route], "Incorrect routes returned" ); + + let all_routes_result_paginated = get_all_swap_routes(deps.as_ref().storage, None, Some(1u32)).unwrap(); + assert_eq!(all_routes_result_paginated.len(), 1); } diff --git a/contracts/swap/src/testing/test_utils.rs b/contracts/swap/src/testing/test_utils.rs index 21935e5..b0c2c5a 100644 --- a/contracts/swap/src/testing/test_utils.rs +++ b/contracts/swap/src/testing/test_utils.rs @@ -2,22 +2,6 @@ use crate::helpers::Scaled; use cosmwasm_std::testing::{MockApi, MockStorage}; use cosmwasm_std::{coin, to_json_binary, Addr, Coin, ContractResult, OwnedDeps, QuerierResult, SystemError, SystemResult}; -use injective_std::{ - shim::Any, - types::{ - cosmos::{ - bank::v1beta1::{MsgSend, QueryAllBalancesRequest, QueryBalanceRequest}, - base::v1beta1::Coin as TubeCoin, - gov::{v1::MsgVote, v1beta1::MsgSubmitProposal}, - }, - injective::exchange::v1beta1::{ - MsgCreateSpotLimitOrder, MsgInstantSpotMarketLaunch, OrderInfo, OrderType, QuerySpotMarketsRequest, SpotMarketParamUpdateProposal, - SpotOrder, - }, - }, -}; -use injective_test_tube::{Account, Bank, Exchange, Gov, InjectiveTestApp, Module, SigningAccount, Wasm}; - use injective_cosmwasm::{ create_orderbook_response_handler, create_spot_multi_market_handler, get_default_subaccount_id_for_checked_address, inj_mock_deps, test_market_ids, HandlesMarketIdQuery, InjectiveQueryWrapper, MarketId, PriceLevel, QueryMarketAtomicExecutionFeeMultiplierResponse, SpotMarket, @@ -31,21 +15,23 @@ use injective_std::{ authz::v1beta1::{Grant, MsgGrant}, bank::v1beta1::{MsgSend, QueryAllBalancesRequest, QueryBalanceRequest}, base::v1beta1::Coin as TubeCoin, - gov::v1::MsgVote, - gov::v1beta1::MsgSubmitProposal, + gov::{v1::MsgVote, v1beta1::MsgSubmitProposal}, }, cosmwasm::wasm::v1::{AcceptedMessageKeysFilter, ContractExecutionAuthorization, ContractGrant, MaxCallsLimit}, injective::exchange::v1beta1::{ - MsgCreateSpotLimitOrder, MsgInstantSpotMarketLaunch, OrderInfo, OrderType, QuerySpotMarketsRequest, SpotMarketParamUpdateProposal, - SpotOrder, + MsgCreateSpotLimitOrder, OrderInfo, OrderType, QuerySpotMarketsRequest, SpotMarketParamUpdateProposal, SpotOrder, }, }, }; use injective_test_tube::{Account, Authz, Bank, Exchange, Gov, InjectiveTestApp, Module, SigningAccount, Wasm}; +use injective_testing::test_tube::{exchange::launch_spot_market_custom, utils::store_code}; +use prost::Message; use std::{collections::HashMap, str::FromStr}; -use crate::msg::{ExecuteMsg, FeeRecipient, InstantiateMsg}; -use crate::types::FPCoin; +use crate::{ + msg::{ExecuteMsg, FeeRecipient, InstantiateMsg}, + types::FPCoin, +}; pub const TEST_CONTRACT_ADDR: &str = "inj14hj2tavq8fpesdwxxcu44rty3hh90vhujaxlnz"; pub const TEST_USER_ADDR: &str = "inj1p7z8p649xspcey7wp5e4leqf7wa39kjjj6wja8"; @@ -288,57 +274,8 @@ fn create_mock_spot_market(base: &str, min_price_tick_size: FPDecimal, min_quant status: injective_cosmwasm::MarketStatus::Active, min_price_tick_size, min_quantity_tick_size, - min_notional: "0".to_string(), } } - -pub fn launch_spot_market(exchange: &Exchange, signer: &SigningAccount, base: &str, quote: &str) -> String { - let ticker = format!("{base}/{quote}"); - exchange - .instant_spot_market_launch( - MsgInstantSpotMarketLaunch { - sender: signer.address(), - ticker: ticker.clone(), - base_denom: base.to_string(), - quote_denom: quote.to_string(), - min_price_tick_size: "1_000_000_000_000_000".to_owned(), - min_quantity_tick_size: "1_000_000_000_000_000".to_owned(), - min_notional: "0".to_string(), - }, - signer, - ) - .unwrap(); - - get_spot_market_id(exchange, ticker) -} - -pub fn launch_custom_spot_market( - exchange: &Exchange, - signer: &SigningAccount, - base: &str, - quote: &str, - min_price_tick_size: &str, - min_quantity_tick_size: &str, -) -> String { - let ticker = format!("{base}/{quote}"); - exchange - .instant_spot_market_launch( - MsgInstantSpotMarketLaunch { - sender: signer.address(), - ticker: ticker.clone(), - base_denom: base.to_string(), - quote_denom: quote.to_string(), - min_price_tick_size: min_price_tick_size.to_string(), - min_quantity_tick_size: min_quantity_tick_size.to_string(), - min_notional: "0".to_string(), - }, - signer, - ) - .unwrap(); - - get_spot_market_id(exchange, ticker) -} - pub fn get_spot_market_id(exchange: &Exchange, ticker: String) -> String { let spot_markets = exchange .query_spot_markets(&QuerySpotMarketsRequest { @@ -354,57 +291,62 @@ pub fn get_spot_market_id(exchange: &Exchange, ticker: String) } pub fn launch_realistic_inj_usdt_spot_market(exchange: &Exchange, signer: &SigningAccount) -> String { - launch_custom_spot_market( + launch_spot_market_custom( exchange, signer, - INJ_2, - USDT, - dec_to_proto(FPDecimal::must_from_str("0.000000000000001")).as_str(), - dec_to_proto(FPDecimal::must_from_str("1000000000000000")).as_str(), + "TICKER".to_string(), + INJ_2.to_string(), + USDT.to_string(), + dec_to_proto(FPDecimal::must_from_str("0.000000000000001")), + dec_to_proto(FPDecimal::must_from_str("0.0001")), ) } pub fn launch_realistic_weth_usdt_spot_market(exchange: &Exchange, signer: &SigningAccount) -> String { - launch_custom_spot_market( + launch_spot_market_custom( exchange, signer, - ETH, - USDT, - dec_to_proto(FPDecimal::must_from_str("0.0000000000001")).as_str(), - dec_to_proto(FPDecimal::must_from_str("1000000000000000")).as_str(), + "ETH/USDT".to_string(), + ETH.to_string(), + USDT.to_string(), + "0.00000000000001".to_string(), + "0.001".to_string(), ) } pub fn launch_realistic_atom_usdt_spot_market(exchange: &Exchange, signer: &SigningAccount) -> String { - launch_custom_spot_market( + launch_spot_market_custom( exchange, signer, - ATOM, - USDT, - dec_to_proto(FPDecimal::must_from_str("0.001")).as_str(), - dec_to_proto(FPDecimal::must_from_str("10000")).as_str(), + "ATOM/USDT".to_string(), + ATOM.to_string(), + USDT.to_string(), + "0.000000000001".to_string(), + "0.000000001".to_string(), ) } pub fn launch_realistic_usdt_usdc_spot_market(exchange: &Exchange, signer: &SigningAccount) -> String { - launch_custom_spot_market( + launch_spot_market_custom( exchange, signer, - USDT, - USDC, - dec_to_proto(FPDecimal::must_from_str("0.0001")).as_str(), - dec_to_proto(FPDecimal::must_from_str("100")).as_str(), + "TICKER".to_string(), + USDT.to_string(), + USDC.to_string(), + dec_to_proto(FPDecimal::must_from_str("0.0001")), + dec_to_proto(FPDecimal::must_from_str("100")), ) } pub fn launch_realistic_ninja_inj_spot_market(exchange: &Exchange, signer: &SigningAccount) -> String { - launch_custom_spot_market( + launch_spot_market_custom( exchange, signer, - NINJA, - INJ_2, - dec_to_proto(FPDecimal::must_from_str("1000000")).as_str(), - dec_to_proto(FPDecimal::must_from_str("10000000")).as_str(), + "TICKER".to_string(), + NINJA.to_string(), + INJ_2.to_string(), + dec_to_proto(FPDecimal::must_from_str("1000000")), + dec_to_proto(FPDecimal::must_from_str("10000000")), ) } @@ -792,7 +734,7 @@ pub fn pause_spot_market(app: &InjectiveTestApp, market_id: &str, proposer: &Sig status: 2, min_notional: "0".to_string(), ticker: "0".to_string(), - admin_info: "0".to_string(), + admin_info: None, }, proposer, validator, diff --git a/contracts/swap/src/types.rs b/contracts/swap/src/types.rs index 34669e9..cd1123b 100644 --- a/contracts/swap/src/types.rs +++ b/contracts/swap/src/types.rs @@ -2,8 +2,6 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Coin}; use injective_cosmwasm::MarketId; use injective_math::FPDecimal; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; #[cw_serde] pub enum SwapEstimationAmount {