From 0aa058fbbcb3431a7ed1b6e46d2ab9da8215856e Mon Sep 17 00:00:00 2001 From: maxrobot Date: Wed, 30 Oct 2024 15:20:13 +0000 Subject: [PATCH 1/9] feat: updated the swap routes query --- Cargo.lock | 2316 ++++++++++------- Cargo.toml | 46 +- Changelog.md | 3 +- contracts/swap/.gitignore | 15 - contracts/swap/.gitpod.Dockerfile | 17 - contracts/swap/.gitpod.yml | 10 - contracts/swap/Cargo.toml | 49 +- contracts/swap/msg.rs | 70 - contracts/swap/rustfmt.toml | 15 - contracts/swap/src/contract.rs | 179 +- contracts/swap/src/msg.rs | 22 +- contracts/swap/src/state.rs | 27 +- contracts/swap/src/swap.rs | 110 +- .../src/testing/integration_logic_tests.rs | 2142 --------------- ...egration_realistic_tests_exact_quantity.rs | 1795 ------------- ...ntegration_realistic_tests_min_quantity.rs | 1425 ---------- contracts/swap/src/testing/queries_tests.rs | 4 +- contracts/swap/src/testing/test_utils.rs | 423 +-- contracts/swap/src/types.rs | 32 +- docs/admin.md | 71 + docs/copy_to_devnet_setup.sh | 13 + docs/examples.sh | 41 + rust-toolchain.toml | 2 +- 23 files changed, 1736 insertions(+), 7091 deletions(-) delete mode 100644 contracts/swap/.gitignore delete mode 100644 contracts/swap/.gitpod.Dockerfile delete mode 100644 contracts/swap/.gitpod.yml delete mode 100644 contracts/swap/msg.rs delete mode 100644 contracts/swap/rustfmt.toml delete mode 100644 contracts/swap/src/testing/integration_logic_tests.rs delete mode 100644 contracts/swap/src/testing/integration_realistic_tests_exact_quantity.rs delete mode 100644 contracts/swap/src/testing/integration_realistic_tests_min_quantity.rs create mode 100644 docs/admin.md create mode 100755 docs/copy_to_devnet_setup.sh create mode 100644 docs/examples.sh diff --git a/Cargo.lock b/Cargo.lock index af5fa65..29e5d74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,56 +2,184 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "ahash" -version = "0.7.6" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ - "getrandom", + "cfg-if", "once_cell", "version_check", + "zerocopy", ] [[package]] name = "aho-corasick" -version = "1.0.1" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] -name = "android-tzdata" -version = "0.1.1" +name = "allocator-api2" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] -name = "android_system_properties" -version = "0.1.5" +name = "anyhow" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" + +[[package]] +name = "ark-bls12-381" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c775f0d12169cba7aae4caeb547bb6a50781c7449a8aa53793827c9ec4abf488" dependencies = [ - "libc", + "ark-ec", + "ark-ff", + "ark-serialize", + "ark-std", ] [[package]] -name = "anyhow" -version = "1.0.71" +name = "ark-ec" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" +dependencies = [ + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", + "itertools 0.10.5", + "num-traits", + "rayon", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint", + "num-traits", + "paste", + "rayon", + "rustc_version", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-poly" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" +dependencies = [ + "ark-ff", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.5", + "rayon", +] [[package]] name = "async-trait" -version = "0.1.68" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.85", ] [[package]] @@ -67,15 +195,24 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] -name = "base16ct" -version = "0.1.1" +name = "backtrace" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] [[package]] name = "base16ct" @@ -85,15 +222,15 @@ checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base64" -version = "0.13.1" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.21.7" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" @@ -103,9 +240,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bech32" -version = "0.9.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" [[package]] name = "bindgen" @@ -113,7 +250,7 @@ version = "0.60.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "062dddbc1ba4aca46de6338e2bf87771414c335f7b2f2036e8f3e9befebf88e6" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cexpr", "clang-sys", "clap", @@ -132,18 +269,16 @@ dependencies = [ [[package]] name = "bip32" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e40748d60a3296653e45e87e64c6989aebfad607bccce59cc4156c5d81b2f70" +checksum = "aa13fae8b6255872fd86f7faf4b41168661d7d78609f7bfe6771b85c6739a15b" dependencies = [ "bs58", "hmac", - "k256 0.13.1", - "once_cell", - "pbkdf2", + "k256", "rand_core 0.6.4", "ripemd", - "sha2 0.10.6", + "sha2 0.10.8", "subtle", "zeroize", ] @@ -154,6 +289,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + [[package]] name = "block-buffer" version = "0.9.0" @@ -174,45 +315,48 @@ dependencies = [ [[package]] name = "bnum" -version = "0.8.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab9008b6bb9fc80b5277f2fe481c09e828743d9151203e804583eb4c9e15b31d" +checksum = "3e31ea183f6ee62ac8b8a8cf7feddd766317adfb13ff469de57ce033efd6a790" [[package]] name = "bs58" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" dependencies = [ - "sha2 0.9.9", + "sha2 0.10.8", ] [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.4.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" dependencies = [ "serde", ] [[package]] name = "cc" -version = "1.0.79" +version = "1.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +dependencies = [ + "shlex", +] [[package]] name = "cexpr" @@ -231,23 +375,18 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", "num-traits", - "wasm-bindgen", - "windows-targets 0.48.0", ] [[package]] name = "clang-sys" -version = "1.6.1" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", @@ -261,9 +400,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "atty", - "bitflags", + "bitflags 1.3.2", "clap_lex", - "indexmap", + "indexmap 1.9.3", "strsim", "termcolor", "textwrap", @@ -284,20 +423,20 @@ version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] name = "const-oid" -version = "0.9.2" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -305,70 +444,118 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cosmos-sdk-proto" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73c9d2043a9e617b0d602fbc0a0ecd621568edbf3a9774890a6d562389bd8e1c" +checksum = "32560304ab4c365791fd307282f76637213d8083c1a98490c35159cd67852237" dependencies = [ - "prost 0.11.9", + "prost 0.12.6", "prost-types", - "tendermint-proto", + "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.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af13955d6f356272e6def9ff5e2450a7650df536d8934f47052a20c76513d2f6" +checksum = "47126f5364df9387b9d8559dcef62e99010e1d4098f39eb3f7ee4b5c254e40ea" dependencies = [ "bip32", - "cosmos-sdk-proto", - "ecdsa 0.16.7", + "cosmos-sdk-proto 0.20.0", + "ecdsa", "eyre", - "getrandom", - "k256 0.13.1", + "k256", "rand_core 0.6.4", "serde", "serde_json", + "signature", "subtle-encoding", - "tendermint", - "tendermint-rpc", + "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", + "ecdsa", + "eyre", + "k256", + "rand_core 0.6.4", + "serde", + "serde_json", + "signature", + "subtle-encoding", + "tendermint 0.39.1", + "tendermint-rpc 0.39.1", + "thiserror", +] + +[[package]] +name = "cosmwasm-core" +version = "2.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6ceb8624260d0d3a67c4e1a1d43fc7e9406720afbcb124521501dd138f90aa" + [[package]] name = "cosmwasm-crypto" -version = "1.5.1" +version = "2.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad24bc9dae9aac5dc124b4560e3f7678729d701f1bf3cb11140703d91f247d31" +checksum = "4125381e5fd7fefe9f614640049648088015eca2b60d861465329a5d87dfa538" dependencies = [ + "ark-bls12-381", + "ark-ec", + "ark-ff", + "ark-serialize", + "cosmwasm-core", "digest 0.10.7", - "ecdsa 0.16.7", + "ecdsa", "ed25519-zebra", - "k256 0.13.1", + "k256", + "num-traits", + "p256", "rand_core 0.6.4", + "rayon", + "sha2 0.10.8", "thiserror", ] [[package]] name = "cosmwasm-derive" -version = "1.5.1" +version = "2.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca65635b768406eabdd28ba015cc3f2f863ca5a2677a7dc4c237b8ee1298efb3" +checksum = "1b5658b1dc64e10b56ae7a449f678f96932a96f6cfad1769d608d1d1d656480a" dependencies = [ - "syn 1.0.109", + "proc-macro2", + "quote", + "syn 2.0.85", ] [[package]] name = "cosmwasm-schema" -version = "1.5.1" +version = "2.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e2a6ce6dbcad572495fd9d9c1072793fe682aebfcc09752c3b0de3fa1814d7" +checksum = "f86b4d949b6041519c58993a73f4bbfba8083ba14f7001eae704865a09065845" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -379,79 +566,83 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.5.1" +version = "2.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904408dc6d73fd1d535c764a55370803cccf6b9be5af7423c4db8967058673f0" +checksum = "c8ef1b5835a65fcca3ab8b9a02b4f4dacc78e233a5c2f20b270efb9db0666d12" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.85", ] [[package]] name = "cosmwasm-std" -version = "1.5.1" +version = "2.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed4564772e5d779235f2d7353e3d66e37793065c3a5155a2978256bf4c5b7d5" +checksum = "70eb7ab0c1e99dd6207496963ba2a457c4128ac9ad9c72a83f8d9808542b849b" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "bech32", "bnum", + "cosmwasm-core", "cosmwasm-crypto", "cosmwasm-derive", - "derivative", - "forward_ref", + "derive_more", "hex", + "rand_core 0.6.4", "schemars", "serde", - "serde-json-wasm 0.5.1", - "sha2 0.10.6", + "serde-json-wasm", + "sha2 0.10.8", "static_assertions 1.1.0", "thiserror", ] [[package]] -name = "cosmwasm-storage" -version = "1.5.0" +name = "cpufeatures" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd2b4ae72a03e8f56c85df59d172d51d2d7dc9cec6e2bc811e3fb60c588032a4" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ - "cosmwasm-std", - "serde", + "libc", ] [[package]] -name = "cpufeatures" -version = "0.2.7" +name = "crossbeam-deque" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ - "libc", + "crossbeam-epoch", + "crossbeam-utils", ] [[package]] -name = "crunchy" -version = "0.2.2" +name = "crossbeam-epoch" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] [[package]] -name = "crypto-bigint" -version = "0.4.9" +name = "crossbeam-utils" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-bigint" -version = "0.5.2" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4c2f4e1afd912bc40bfd6fed5d9dc1f288e0ba01bfcc835cc5bc3eb13efe15" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -470,25 +661,30 @@ dependencies = [ ] [[package]] -name = "ct-logs" -version = "0.8.0" +name = "curve25519-dalek" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1a816186fa68d9e426e3cb4ae4dff1fcd8e4a2c34b781bf7a822574a0d0aac8" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ - "sct", + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", ] [[package]] -name = "curve25519-dalek" -version = "3.2.0" +name = "curve25519-dalek-derive" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.5.1", - "subtle", - "zeroize", + "proc-macro2", + "quote", + "syn 2.0.85", ] [[package]] @@ -504,30 +700,11 @@ dependencies = [ "zeroize", ] -[[package]] -name = "cw-multi-test" -version = "0.16.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a18afd2e201221c6d72a57f0886ef2a22151bbc9e6db7af276fde8a91081042" -dependencies = [ - "anyhow", - "cosmwasm-std", - "cw-storage-plus 1.2.0", - "cw-utils 1.0.1", - "derivative", - "itertools", - "k256 0.11.6", - "prost 0.9.0", - "schemars", - "serde", - "thiserror", -] - [[package]] name = "cw-storage-plus" -version = "0.14.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c8b264257c4f44c49b7ce09377af63aa040768ecd3fd7bdd2d48a09323a1e90" +checksum = "f13360e9007f51998d42b1bc6b7fa0141f74feae61ed5fd1e5b0a89eec7b5de1" dependencies = [ "cosmwasm-std", "schemars", @@ -535,24 +712,27 @@ dependencies = [ ] [[package]] -name = "cw-storage-plus" -version = "1.2.0" +name = "cw-utils" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5ff29294ee99373e2cd5fd21786a3c0ced99a52fec2ca347d565489c61b723c" +checksum = "07dfee7f12f802431a856984a32bce1cb7da1e6c006b5409e3981035ce562dec" dependencies = [ + "cosmwasm-schema", "cosmwasm-std", "schemars", "serde", + "thiserror", ] [[package]] -name = "cw-utils" -version = "0.14.0" +name = "cw2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414b91f3d7a619bb26c835119d7095804596a1382ddc1d184c33c1d2c17f6c5e" +checksum = "b04852cd38f044c0751259d5f78255d07590d136b8a86d4e09efdd7666bd6d27" dependencies = [ + "cosmwasm-schema", "cosmwasm-std", - "cw2 0.14.0", + "cw-storage-plus", "schemars", "semver", "serde", @@ -560,63 +740,56 @@ dependencies = [ ] [[package]] -name = "cw-utils" -version = "1.0.1" +name = "darling" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c80e93d1deccb8588db03945016a292c3c631e6325d349ebb35d2db6f4f946f7" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw2 1.0.1", - "schemars", - "semver", - "serde", - "thiserror", + "darling_core", + "darling_macro", ] [[package]] -name = "cw2" -version = "0.14.0" +name = "darling_core" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa74c324af8e3506fd8d50759a265bead3f87402e413c840042af5d2808463d6" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ - "cosmwasm-std", - "cw-storage-plus 0.14.0", - "schemars", - "serde", + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.85", ] [[package]] -name = "cw2" -version = "1.0.1" +name = "darling_macro" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb70cee2cf0b4a8ff7253e6bc6647107905e8eb37208f87d54f67810faa62f8" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-storage-plus 1.2.0", - "schemars", - "serde", + "darling_core", + "quote", + "syn 2.0.85", ] [[package]] name = "der" -version = "0.6.1" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", "zeroize", ] [[package]] -name = "der" -version = "0.7.6" +name = "deranged" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56acb310e15652100da43d130af8d97b509e95af61aab1c5a7939ef24337ee17" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ - "const-oid", - "zeroize", + "powerfmt", ] [[package]] @@ -630,6 +803,27 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", + "unicode-xid", +] + [[package]] name = "digest" version = "0.9.0" @@ -653,44 +847,32 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" - -[[package]] -name = "ecdsa" -version = "0.14.8" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" -dependencies = [ - "der 0.6.1", - "elliptic-curve 0.12.3", - "rfc6979 0.3.1", - "signature 1.6.4", -] +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "ecdsa" -version = "0.16.7" +version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0997c976637b606099b9985693efa3581e84e41f5c11ba5255f88711058ad428" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der 0.7.6", + "der", "digest 0.10.7", - "elliptic-curve 0.13.5", - "rfc6979 0.4.0", - "signature 2.1.0", - "spki 0.7.2", + "elliptic-curve", + "rfc6979", + "signature", + "spki", ] [[package]] name = "ed25519" -version = "2.2.1" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fb04eee5d9d907f29e80ee6b0e78f7e2c82342c63e3580d8c4f69d9d5aad963" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8 0.10.2", - "signature 2.1.0", + "pkcs8", + "signature", ] [[package]] @@ -708,62 +890,72 @@ dependencies = [ [[package]] name = "ed25519-zebra" -version = "3.1.0" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" +checksum = "7d9ce6874da5d4415896cd45ffbc4d1cfc0c4f9c079427bd870742c30f2f65a9" dependencies = [ "curve25519-dalek", - "hashbrown", + "ed25519", + "hashbrown 0.14.5", "hex", "rand_core 0.6.4", - "serde", - "sha2 0.9.9", + "sha2 0.10.8", "zeroize", ] [[package]] name = "either" -version = "1.8.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "elliptic-curve" -version = "0.12.3" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "base16ct 0.1.1", - "crypto-bigint 0.4.9", - "der 0.6.1", + "base16ct", + "crypto-bigint", "digest 0.10.7", - "ff 0.12.1", + "ff", "generic-array", - "group 0.12.1", - "pkcs8 0.9.0", + "group", + "pkcs8", "rand_core 0.6.4", - "sec1 0.3.0", + "sec1", "subtle", "zeroize", ] [[package]] -name = "elliptic-curve" -version = "0.13.5" +name = "encoding_rs" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ - "base16ct 0.2.0", - "crypto-bigint 0.5.2", - "digest 0.10.7", - "ff 0.13.0", - "generic-array", - "group 0.13.0", - "pkcs8 0.10.2", - "rand_core 0.6.4", - "sec1 0.7.2", - "subtle", - "zeroize", + "cfg-if", +] + +[[package]] +name = "enumset" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a4b049558765cef5f0c1a273c3fc57084d768b44d2f98127aef4cceb17293" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59c3b24c345d8c314966bdc1832f6c2635bfcce8e7cf363bd115987bba2ee242" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.85", ] [[package]] @@ -779,6 +971,22 @@ dependencies = [ "termcolor", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "ethbloom" version = "0.6.4" @@ -817,9 +1025,9 @@ dependencies = [ [[package]] name = "eyre" -version = "0.6.8" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" dependencies = [ "indenter", "once_cell", @@ -827,23 +1035,19 @@ dependencies = [ [[package]] name = "ff" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ "rand_core 0.6.4", "subtle", ] [[package]] -name = "ff" -version = "0.13.0" +name = "fiat-crypto" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "fixed-hash" @@ -853,7 +1057,7 @@ checksum = "d1a683d1234507e4f3bf2736eeddf0de1dc65996dc0164d57eba0a74bcf29489" dependencies = [ "byteorder", "heapsize", - "rand", + "rand 0.5.6", "rustc-hex", "static_assertions 0.2.5", ] @@ -885,19 +1089,13 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] -[[package]] -name = "forward_ref" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" - [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -906,13 +1104,12 @@ checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" [[package]] name = "futures" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", - "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -921,9 +1118,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -931,66 +1128,39 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" - -[[package]] -name = "futures-executor" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" - -[[package]] -name = "futures-macro" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.48", -] +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ - "futures-channel", "futures-core", - "futures-io", - "futures-macro", "futures-sink", "futures-task", - "memchr", "pin-project-lite", "pin-utils", - "slab", ] [[package]] @@ -1006,9 +1176,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.9" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "js-sys", @@ -1018,21 +1188,16 @@ dependencies = [ ] [[package]] -name = "glob" -version = "0.3.1" +name = "gimli" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] -name = "group" -version = "0.12.1" +name = "glob" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" -dependencies = [ - "ff 0.12.1", - "rand_core 0.6.4", - "subtle", -] +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "group" @@ -1040,16 +1205,16 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff 0.13.0", + "ff", "rand_core 0.6.4", "subtle", ] [[package]] name = "h2" -version = "0.3.19" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", @@ -1057,7 +1222,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 2.6.0", "slab", "tokio", "tokio-util", @@ -1069,34 +1234,31 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ "ahash", ] [[package]] -name = "headers" -version = "0.3.8" +name = "hashbrown" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "base64 0.13.1", - "bitflags", - "bytes", - "headers-core", - "http", - "httpdate", - "mime", - "sha1", + "ahash", + "allocator-api2", ] [[package]] -name = "headers-core" -version = "0.2.0" +name = "hashbrown" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" -dependencies = [ - "http", -] +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" [[package]] name = "heapsize" @@ -1118,12 +1280,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.2.6" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -1143,11 +1302,20 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "http" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", @@ -1156,9 +1324,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http", @@ -1167,15 +1335,15 @@ dependencies = [ [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" @@ -1185,9 +1353,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.26" +version = "0.14.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" +checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" dependencies = [ "bytes", "futures-channel", @@ -1207,71 +1375,31 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-proxy" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca815a891b24fdfb243fa3239c86154392b0953ee584aa1a2a1f66d20cbe75cc" -dependencies = [ - "bytes", - "futures", - "headers", - "http", - "hyper", - "hyper-rustls", - "rustls-native-certs", - "tokio", - "tokio-rustls", - "tower-service", - "webpki", -] - [[package]] name = "hyper-rustls" -version = "0.22.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f9f7a97316d44c0af9b0301e65010573a853a9fc97046d7331d7f6bc0fd5a64" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ - "ct-logs", "futures-util", + "http", "hyper", - "log", "rustls", - "rustls-native-certs", "tokio", "tokio-rustls", - "webpki", - "webpki-roots", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows", ] [[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" +name = "ident_case" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1308,17 +1436,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.0", ] [[package]] name = "injective-cosmwasm" -version = "0.2.18" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b6f08b14a23696948d51ba6a050382cbc6e23522efec3ca607a5b1a317de0c" +checksum = "a551fbe7bae0747a41ce81a1e7d5ba96ef089a7f0b3f05ab5a9b510248a709a7" dependencies = [ "cosmwasm-std", - "cw-storage-plus 1.2.0", + "cw-storage-plus", "ethereum-types", "hex", "injective-math", @@ -1331,9 +1469,9 @@ dependencies = [ [[package]] name = "injective-math" -version = "0.2.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4e31ffb7dff274e0be1117bc8f1240f6572d6157be2c4daf13ff82eaaddd85" +checksum = "194fb5cb49537b0b9137d02a563b7019003220fb4affff05ad6cdc6fee3509c9" dependencies = [ "cosmwasm-std", "ethereum-types", @@ -1344,57 +1482,60 @@ dependencies = [ ] [[package]] -name = "injective-protobuf" -version = "0.2.2" +name = "injective-std" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a52219a08aba8c17846fd23d472d1d69c817fe5b427d135273e4c7311edd6972" +checksum = "c0e5193cb9520754f60b9e9af08a662ddf298d2e1a579200b9a447064b64db8b" dependencies = [ + "chrono", "cosmwasm-std", - "ethereum-types", - "num", - "protobuf", - "protobuf-codegen-pure", + "injective-std-derive", + "prost 0.12.6", + "prost-types", "schemars", "serde", - "subtle-encoding", + "serde-cw-value", ] [[package]] -name = "injective-std" -version = "0.1.5" +name = "injective-std-derive" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7a5b52d19dca05823c7e4b481d41b49c04a0e56f66a5c92396a6fdd3314710" +checksum = "2721d8c2fed1fd1dff4cd6d119711a74acf27a6eeea6bf09cd44d192119e52ea" dependencies = [ - "chrono", "cosmwasm-std", - "osmosis-std-derive", - "prost 0.11.9", - "prost-types", - "schemars", - "serde", - "serde-cw-value", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] name = "injective-test-tube" -version = "1.1.7" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61cb772fd4c8c1da872b742633e73928cbf1cf0505a09ec8ad41934a4d7f10b4" +checksum = "2a45747c74fca8aedafd94df74c6b9edf091c586ead96957e3c17e96abd6228b" dependencies = [ - "base64 0.13.1", + "base64 0.21.7", "bindgen", - "cosmrs", + "cosmrs 0.15.0", "cosmwasm-std", "hex", "injective-cosmwasm", "injective-std", - "prost 0.11.9", + "prost 0.12.6", "serde", "serde_json", "test-tube-inj", "thiserror", ] +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + [[package]] name = "itertools" version = "0.10.5" @@ -1405,51 +1546,55 @@ dependencies = [ ] [[package]] -name = "itoa" -version = "1.0.6" +name = "itertools" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] [[package]] -name = "js-sys" -version = "0.3.63" +name = "itertools" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ - "wasm-bindgen", + "either", ] [[package]] -name = "k256" -version = "0.11.6" +name = "itoa" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ - "cfg-if", - "ecdsa 0.14.8", - "elliptic-curve 0.12.3", - "sha2 0.10.6", + "wasm-bindgen", ] [[package]] name = "k256" -version = "0.13.1" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ "cfg-if", - "ecdsa 0.16.7", - "elliptic-curve 0.13.5", - "once_cell", - "sha2 0.10.6", - "signature 2.1.0", + "ecdsa", + "elliptic-curve", + "sha2 0.10.8", ] [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lazycell" @@ -1459,34 +1604,37 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.144" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libloading" -version = "0.7.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "winapi", + "windows-targets 0.52.6", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "log" -version = "0.4.17" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" -version = "2.5.0" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" @@ -1500,16 +1648,25 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + [[package]] name = "mio" -version = "0.8.6" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ + "hermit-abi 0.3.9", "libc", - "log", "wasi", - "windows-sys 0.45.0", + "windows-sys 0.52.0", ] [[package]] @@ -1522,39 +1679,21 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "num" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "autocfg", "num-integer", "num-traits", ] [[package]] -name = "num-complex" -version = "0.4.3" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d" -dependencies = [ - "num-traits", -] +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-derive" @@ -1569,67 +1708,42 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.1" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", - "num-bigint", - "num-integer", "num-traits", ] [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] -name = "num_cpus" -version = "1.15.0" +name = "object" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ - "hermit-abi 0.2.6", - "libc", + "memchr", ] [[package]] name = "once_cell" -version = "1.17.1" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "opaque-debug" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl-probe" @@ -1639,37 +1753,27 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "os_str_bytes" -version = "6.5.0" +version = "6.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" [[package]] -name = "osmosis-std-derive" -version = "0.15.3" +name = "p256" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4d482a16be198ee04e0f94e10dd9b8d02332dcf33bc5ea4b255e7e25eedc5df" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ - "itertools", - "proc-macro2", - "quote", - "syn 1.0.109", + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.8", ] [[package]] name = "paste" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" - -[[package]] -name = "pbkdf2" -version = "0.12.1" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0ca0b5a68607598bf3bad68f32227a8164f6254833f84eafaac409cd6746c31" -dependencies = [ - "digest 0.10.7", - "hmac", -] +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "peeking_take_while" @@ -1679,9 +1783,9 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "peg" -version = "0.7.0" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c0b841ea54f523f7aa556956fbd293bcbe06f2e67d2eb732b7278aaf1d166a" +checksum = "295283b02df346d1ef66052a757869b2876ac29a6bb0ac3f5f7cd44aebe40e8f" dependencies = [ "peg-macros", "peg-runtime", @@ -1689,9 +1793,9 @@ dependencies = [ [[package]] name = "peg-macros" -version = "0.7.0" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aa52829b8decbef693af90202711348ab001456803ba2a98eb4ec8fb70844c" +checksum = "bdad6a1d9cf116a059582ce415d5f5566aabcd4008646779dab7fdc2a9a9d426" dependencies = [ "peg-runtime", "proc-macro2", @@ -1700,41 +1804,41 @@ dependencies = [ [[package]] name = "peg-runtime" -version = "0.7.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c719dcf55f09a3a7e764c6649ab594c18a177e3599c467983cdf644bfc0a4088" +checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a" [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.0" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.0" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.85", ] [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -1744,22 +1848,36 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkcs8" -version = "0.9.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der 0.6.1", - "spki 0.6.0", + "der", + "spki", ] [[package]] -name = "pkcs8" -version = "0.10.2" +name = "powerfmt" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" dependencies = [ - "der 0.7.6", - "spki 0.7.2", + "elliptic-curve", ] [[package]] @@ -1774,101 +1892,73 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.76" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] [[package]] name = "prost" -version = "0.9.0" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ "bytes", - "prost-derive 0.9.0", + "prost-derive 0.12.6", ] [[package]] name = "prost" -version = "0.11.9" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f" dependencies = [ "bytes", - "prost-derive 0.11.9", + "prost-derive 0.13.3", ] [[package]] name = "prost-derive" -version = "0.9.0" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", - "itertools", + "itertools 0.12.1", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.85", ] [[package]] name = "prost-derive" -version = "0.11.9" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" dependencies = [ "anyhow", - "itertools", + "itertools 0.13.0", "proc-macro2", "quote", - "syn 1.0.109", + "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 = "protobuf" -version = "2.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" -dependencies = [ - "bytes", -] - -[[package]] -name = "protobuf-codegen" -version = "2.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "033460afb75cf755fcfc16dfaed20b86468082a2ea24e05ac35ab4a099a017d6" -dependencies = [ - "protobuf", -] - -[[package]] -name = "protobuf-codegen-pure" -version = "2.28.0" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a29399fc94bcd3eeaa951c715f7bea69409b2445356b00519740bcd6ddd865" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" dependencies = [ - "protobuf", - "protobuf-codegen", + "prost 0.12.6", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -1886,6 +1976,27 @@ dependencies = [ "winapi", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_core" version = "0.3.1" @@ -1901,12 +2012,6 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" - [[package]] name = "rand_core" version = "0.6.4" @@ -1916,11 +2021,43 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "regex" -version = "1.8.3" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", @@ -1929,19 +2066,49 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.2" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] -name = "rfc6979" -version = "0.3.1" +name = "reqwest" +version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ - "crypto-bigint 0.4.9", - "hmac", - "zeroize", + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", ] [[package]] @@ -1956,17 +2123,17 @@ dependencies = [ [[package]] name = "ring" -version = "0.16.20" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", + "cfg-if", + "getrandom", "libc", - "once_cell", "spin", "untrusted", - "web-sys", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -1987,6 +2154,12 @@ dependencies = [ "rustc-hex", ] +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -1999,36 +2172,76 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "rustls" -version = "0.19.1" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ - "base64 0.13.1", "log", "ring", + "rustls-webpki", "sct", - "webpki", ] [[package]] name = "rustls-native-certs" -version = "0.5.0" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ "openssl-probe", - "rustls", + "rustls-pemfile", "schannel", "security-framework", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -2041,20 +2254,21 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.21" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" dependencies = [ - "windows-sys 0.42.0", + "windows-sys 0.59.0", ] [[package]] name = "schemars" -version = "0.8.16" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" dependencies = [ "dyn-clone", + "enumset", "schemars_derive", "serde", "serde_json", @@ -2062,21 +2276,21 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.16" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 1.0.109", + "syn 2.0.85", ] [[package]] name = "sct" -version = "0.6.1" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ "ring", "untrusted", @@ -2084,39 +2298,25 @@ dependencies = [ [[package]] name = "sec1" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" -dependencies = [ - "base16ct 0.1.1", - "der 0.6.1", - "generic-array", - "pkcs8 0.9.0", - "subtle", - "zeroize", -] - -[[package]] -name = "sec1" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0aec48e813d6b90b15f0b8948af3c63483992dee44c03e9930b3eebdabe046e" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base16ct 0.2.0", - "der 0.7.6", + "base16ct", + "der", "generic-array", - "pkcs8 0.10.2", + "pkcs8", "subtle", "zeroize", ] [[package]] name = "security-framework" -version = "2.9.1" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -2125,9 +2325,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" dependencies = [ "core-foundation-sys", "libc", @@ -2135,15 +2335,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.195" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" dependencies = [ "serde_derive", ] @@ -2157,15 +2357,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde-json-wasm" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16a62a1fad1e1828b24acac8f2b468971dade7b8c3c2e672bcadefefb1f8c137" -dependencies = [ - "serde", -] - [[package]] name = "serde-json-wasm" version = "1.0.1" @@ -2177,66 +2368,77 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.9" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.195" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.85", ] [[package]] name = "serde_derive_internals" -version = "0.26.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.85", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] [[package]] name = "serde_repr" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "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", ] [[package]] -name = "sha1" -version = "0.10.5" +name = "serde_urlencoded" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.7", + "form_urlencoded", + "itoa", + "ryu", + "serde", ] [[package]] @@ -2254,9 +2456,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -2265,25 +2467,15 @@ dependencies = [ [[package]] name = "shlex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" - -[[package]] -name = "signature" -version = "1.6.4" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" -dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", -] +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signature" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", "rand_core 0.6.4", @@ -2291,47 +2483,37 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "socket2" -version = "0.4.9" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", - "winapi", + "windows-sys 0.52.0", ] [[package]] name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "spki" -version = "0.6.0" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" -dependencies = [ - "base64ct", - "der 0.6.1", -] +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "spki" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der 0.7.6", + "der", ] [[package]] @@ -2354,9 +2536,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "subtle-encoding" @@ -2375,27 +2557,21 @@ checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" [[package]] name = "swap-contract" -version = "1.0.2" +version = "1.1.0" dependencies = [ - "cosmos-sdk-proto", "cosmwasm-schema", "cosmwasm-std", - "cosmwasm-storage", - "cw-multi-test", - "cw-storage-plus 0.14.0", - "cw-utils 0.14.0", - "cw2 0.14.0", + "cw-storage-plus", + "cw-utils", + "cw2", "injective-cosmwasm", "injective-math", - "injective-protobuf", "injective-std", "injective-test-tube", - "num-traits", - "prost 0.11.9", - "protobuf", + "prost 0.12.6", "schemars", "serde", - "serde-json-wasm 1.0.1", + "serde-json-wasm", "thiserror", ] @@ -2412,20 +2588,47 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.48" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tendermint" -version = "0.32.0" +version = "0.34.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a46ec6b25b028097ab682ffae11d09d64fe1e2535833b902f26a278a0f88a705" +checksum = "15ab8f0a25d0d2ad49ac615da054d6a76aa6603ff95f7d18bafdd34450a1a04b" dependencies = [ "bytes", "digest 0.10.7", @@ -2433,50 +2636,94 @@ dependencies = [ "ed25519-consensus", "flex-error", "futures", - "k256 0.13.1", + "k256", "num-traits", "once_cell", - "prost 0.11.9", + "prost 0.12.6", "prost-types", "ripemd", "serde", "serde_bytes", "serde_json", "serde_repr", - "sha2 0.10.6", - "signature 2.1.0", + "sha2 0.10.8", + "signature", + "subtle", + "subtle-encoding", + "tendermint-proto 0.34.1", + "time", + "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", + "serde_bytes", + "serde_json", + "serde_repr", + "sha2 0.10.8", + "signature", "subtle", "subtle-encoding", - "tendermint-proto", + "tendermint-proto 0.39.1", "time", "zeroize", ] [[package]] name = "tendermint-config" -version = "0.32.0" +version = "0.34.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dbb7610ef8422d5886116868e48ab6a9ea72859b3bf9021c6d87318e5600225" +checksum = "e1a02da769166e2052cd537b1a97c78017632c2d9e19266367b27e73910434fc" dependencies = [ "flex-error", "serde", "serde_json", - "tendermint", - "toml", + "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", + "serde_json", + "tendermint 0.39.1", + "toml 0.8.19", "url", ] [[package]] name = "tendermint-proto" -version = "0.32.0" +version = "0.34.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce23c8ff0e6634eb4c3c4aeed45076dc97dac91aac5501a905a67fa222e165b" +checksum = "b797dd3d2beaaee91d2f065e7bdf239dc8d80bba4a183a288bc1279dd5a69a1e" dependencies = [ "bytes", "flex-error", "num-derive", "num-traits", - "prost 0.11.9", + "prost 0.12.6", "prost-types", "serde", "serde_bytes", @@ -2484,32 +2731,78 @@ dependencies = [ "time", ] +[[package]] +name = "tendermint-proto" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3abf34ecf33125621519e9952688e7a59a98232d51538037ba21fbe526a802" +dependencies = [ + "bytes", + "flex-error", + "prost 0.13.3", + "serde", + "serde_bytes", + "subtle-encoding", + "time", +] + [[package]] name = "tendermint-rpc" -version = "0.32.0" +version = "0.34.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd2cc789170db5a35d4e0bb2490035c03ef96df08f119bee25fd8dab5a09aa25" +checksum = "71afae8bb5f6b14ed48d4e1316a643b6c2c3cbad114f510be77b4ed20b7b3e42" +dependencies = [ + "async-trait", + "bytes", + "flex-error", + "futures", + "getrandom", + "peg", + "pin-project", + "rand 0.8.5", + "reqwest", + "semver", + "serde", + "serde_bytes", + "serde_json", + "subtle", + "subtle-encoding", + "tendermint 0.34.1", + "tendermint-config 0.34.1", + "tendermint-proto 0.34.1", + "thiserror", + "time", + "tokio", + "tracing", + "url", + "uuid", + "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", - "http", - "hyper", - "hyper-proxy", - "hyper-rustls", "peg", "pin-project", + "rand 0.8.5", + "reqwest", "semver", "serde", "serde_bytes", "serde_json", "subtle", "subtle-encoding", - "tendermint", - "tendermint-config", - "tendermint-proto", + "tendermint 0.39.1", + "tendermint-config 0.39.1", + "tendermint-proto 0.39.1", "thiserror", "time", "tokio", @@ -2521,61 +2814,63 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.2.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] [[package]] name = "test-tube-inj" -version = "1.1.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae2aa4f0386a041eea657c0d0b4438f1d757a231d0736c1b3413b9c84cfa36db" +checksum = "662c9081865602de48ca4c7cd2dbd6a1645060eea46614eb789c68f20c039707" dependencies = [ - "base64 0.13.1", - "cosmrs", + "base64 0.21.7", + "cosmrs 0.20.0", "cosmwasm-std", - "prost 0.11.9", + "prost 0.13.3", "serde", "serde_json", - "tendermint-proto", "thiserror", ] [[package]] name = "textwrap" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.85", ] [[package]] name = "time" -version = "0.3.21" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ + "deranged", + "num-conv", + "powerfmt", "serde", "time-core", "time-macros", @@ -2583,16 +2878,17 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.9" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ + "num-conv", "time-core", ] @@ -2607,9 +2903,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -2622,55 +2918,52 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.28.1" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" dependencies = [ - "autocfg", + "backtrace", "bytes", "libc", "mio", - "num_cpus", "pin-project-lite", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.85", ] [[package]] name = "tokio-rustls" -version = "0.22.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ "rustls", "tokio", - "webpki", ] [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] @@ -2682,43 +2975,76 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "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", +] + +[[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", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "uint" @@ -2746,36 +3072,42 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-ident" -version = "1.0.9" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "untrusted" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.3.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -2784,21 +3116,21 @@ dependencies = [ [[package]] name = "uuid" -version = "0.8.2" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" -version = "2.3.3" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -2806,11 +3138,10 @@ dependencies = [ [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] @@ -2822,34 +3153,47 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.86" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.86" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.85", "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" -version = "0.2.86" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2857,61 +3201,43 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.86" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.85", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.86" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "web-sys" -version = "0.3.63" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" dependencies = [ "js-sys", "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "webpki-roots" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" -dependencies = [ - "webpki", -] - [[package]] name = "which" -version = "4.4.0" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" dependencies = [ "either", - "libc", + "home", "once_cell", + "rustix", ] [[package]] @@ -2932,11 +3258,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -2945,167 +3271,199 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets 0.48.0", -] - [[package]] name = "windows-sys" -version = "0.42.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows-targets 0.48.5", ] [[package]] name = "windows-sys" -version = "0.45.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.42.2", + "windows-targets 0.52.6", ] [[package]] name = "windows-sys" -version = "0.48.0" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.52.6", ] [[package]] name = "windows-targets" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] [[package]] name = "zeroize" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" dependencies = [ "zeroize_derive", ] @@ -3118,5 +3476,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.85", ] diff --git a/Cargo.toml b/Cargo.toml index 38ef009..9c67df5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,21 +1,35 @@ [workspace] -members = ["contracts/*"] +members = [ "contracts/*" ] +resolver = "2" -[profile.release.package.injective-cosmwasm] -codegen-units = 1 -incremental = false +[workspace.package] +edition = "2021" + +[workspace.dependencies] +cosmwasm-schema = { version = "2.1.1" } +cosmwasm-std = { version = "2.1.0", features = [ "abort", "cosmwasm_1_2", "cosmwasm_1_3", "cosmwasm_1_4", "iterator", "stargate" ] } +cw-multi-test = { version = "0.16.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-math = { version = "0.3.0" } +injective-std = { version = "1.13.0" } +injective-test-tube = { version = "1.13.2" } +prost = { version = "0.12.6" } +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" } +thiserror = { version = "1.0.52" } [profile.release] -opt-level = 3 -debug = false -rpath = false -lto = true +codegen-units = 1 +debug = false debug-assertions = false -codegen-units = 1 -panic = 'abort' -incremental = false -overflow-checks = true - -[patch.crates-io] -#cw-multi-test = { path = "../cw-multi-test" } -#cw-multi-test = { git = "https://github.com/InjectiveLabs/cw-multi-test.git", branch ="feature/custom_address_generator" } \ No newline at end of file +incremental = false +lto = true +opt-level = 3 +overflow-checks = true +panic = 'abort' +rpath = false diff --git a/Changelog.md b/Changelog.md index b2fc3c4..e18d757 100644 --- a/Changelog.md +++ b/Changelog.md @@ -16,10 +16,11 @@ All notable changes to this project will be documented in this file. - -## [1.0.2] - 2024-03-19 +## [1.1.0] - 2024-10-30 ### Fixed +- Added pagination to `get_all_routes` - Bump version of `serde-json-wasm` ## [1.0.1] - 2024-02-24 diff --git a/contracts/swap/.gitignore b/contracts/swap/.gitignore deleted file mode 100644 index dfdaaa6..0000000 --- a/contracts/swap/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -# Build results -/target - -# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) -.cargo-ok - -# Text file backups -**/*.rs.bk - -# macOS -.DS_Store - -# IDEs -*.iml -.idea diff --git a/contracts/swap/.gitpod.Dockerfile b/contracts/swap/.gitpod.Dockerfile deleted file mode 100644 index bff8bc5..0000000 --- a/contracts/swap/.gitpod.Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -### wasmd ### -FROM cosmwasm/wasmd:v0.18.0 as wasmd - -### rust-optimizer ### -FROM cosmwasm/rust-optimizer:0.11.5 as rust-optimizer - -FROM gitpod/workspace-full:latest - -COPY --from=wasmd /usr/bin/wasmd /usr/local/bin/wasmd -COPY --from=wasmd /opt/* /opt/ - -RUN sudo apt-get update \ - && sudo apt-get install -y jq \ - && sudo rm -rf /var/lib/apt/lists/* - -RUN rustup update stable \ - && rustup target add wasm32-unknown-unknown diff --git a/contracts/swap/.gitpod.yml b/contracts/swap/.gitpod.yml deleted file mode 100644 index d03610c..0000000 --- a/contracts/swap/.gitpod.yml +++ /dev/null @@ -1,10 +0,0 @@ -image: cosmwasm/cw-gitpod-base:v0.16 - -vscode: - extensions: - - rust-lang.rust - -tasks: - - name: Dependencies & Build - init: | - cargo build diff --git a/contracts/swap/Cargo.toml b/contracts/swap/Cargo.toml index f9f0608..40fb8f2 100644 --- a/contracts/swap/Cargo.toml +++ b/contracts/swap/Cargo.toml @@ -2,7 +2,7 @@ authors = [ "Markus Waas " ] edition = "2021" name = "swap-contract" -version = "1.0.2" +version = "1.1.0" exclude = [ # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. @@ -16,38 +16,25 @@ exclude = [ crate-type = [ "cdylib", "rlib" ] [features] -# for more explicit tests, cargo test --features=backtraces -backtraces = [ "cosmwasm-std/backtraces" ] -# use library feature to disable all instantiate/execute/query exports library = [ ] -[package.metadata.scripts] -optimize = """docker run --rm -v "$(pwd)":/code \ - --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ - --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ - cosmwasm/workspace-optimizer-arm64:0.12.11 -""" - [dependencies] -cosmwasm-std = { version = "1.5.0", features = [ "abort", "cosmwasm_1_2", "cosmwasm_1_3", "cosmwasm_1_4", "iterator", "stargate" ] } -cosmwasm-storage = "1.5.0" -cw-storage-plus = "0.14.0" -cw-utils = "0.14.0" -cw2 = "0.14.0" -injective-cosmwasm = { version = "0.2.18" } -injective-math = { version = "0.2.4" } -injective-protobuf = { version = "0.2.2" } -num-traits = "0.2.15" -protobuf = { version = "2", features = [ "with-bytes" ] } -schemars = "0.8.8" -serde = { version = "1.0.137", default-features = false, features = [ "derive" ] } -serde-json-wasm = "1.0.1" -thiserror = { version = "1.0.31" } +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +cw2 = { workspace = true } +injective-cosmwasm = { workspace = true } +injective-math = { workspace = true } +injective-std = { workspace = true } +prost = { workspace = true } +schemars = { workspace = true } +serde = { workspace = true } +serde-json-wasm = { workspace = true } +thiserror = { workspace = true } [dev-dependencies] -cosmos-sdk-proto = { version = "0.19.0", default-features = false } -cosmwasm-schema = "1.5.0" -cw-multi-test = "0.16.2" -injective-std = { version = "0.1.5" } -injective-test-tube = "1.1.7" -prost = "0.11.9" +cosmwasm-schema = { workspace = true } +# cw-multi-test = { workspace = true } +injective-std = { workspace = true } +injective-test-tube = { workspace = true } diff --git a/contracts/swap/msg.rs b/contracts/swap/msg.rs deleted file mode 100644 index f0c841e..0000000 --- a/contracts/swap/msg.rs +++ /dev/null @@ -1,70 +0,0 @@ -use cosmwasm_std::{Addr, Coin}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use injective_cosmwasm::MarketId; -use injective_math::FPDecimal; - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum FeeRecipient { - Address(Addr), - SwapContract, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] -pub struct InstantiateMsg { - pub fee_recipient: FeeRecipient, - pub admin: Addr, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum ExecuteMsg { - SwapMinOutput { - target_denom: String, - min_output_quantity: FPDecimal, - }, - SwapExactOutput { - target_denom: String, - target_output_quantity: FPDecimal, - }, - SetRoute { - source_denom: String, - target_denom: String, - route: Vec, - }, - DeleteRoute { - source_denom: String, - target_denom: String, - }, - UpdateConfig { - admin: Option, - fee_recipient: Option, - }, - WithdrawSupportFunds { - coins: Vec, - target_address: Addr, - }, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum QueryMsg { - GetRoute { - source_denom: String, - target_denom: String, - }, - GetOutputQuantity { - from_quantity: FPDecimal, - source_denom: String, - target_denom: String, - }, - GetInputQuantity { - to_quantity: FPDecimal, - source_denom: String, - target_denom: String, - }, - GetAllRoutes {}, - GetConfig {}, -} diff --git a/contracts/swap/rustfmt.toml b/contracts/swap/rustfmt.toml deleted file mode 100644 index 11a85e6..0000000 --- a/contracts/swap/rustfmt.toml +++ /dev/null @@ -1,15 +0,0 @@ -# stable -newline_style = "unix" -hard_tabs = false -tab_spaces = 4 - -# unstable... should we require `rustup run nightly cargo fmt` ? -# or just update the style guide when they are stable? -#fn_single_line = true -#format_code_in_doc_comments = true -#overflow_delimited_expr = true -#reorder_impl_items = true -#struct_field_align_threshold = 20 -#struct_lit_single_line = true -#report_todo = "Always" - diff --git a/contracts/swap/src/contract.rs b/contracts/swap/src/contract.rs index 3702ecb..88d9f23 100644 --- a/contracts/swap/src/contract.rs +++ b/contracts/swap/src/contract.rs @@ -1,22 +1,17 @@ -#[cfg(not(feature = "library"))] -use cosmwasm_std::entry_point; -use cosmwasm_std::{ - to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, +use crate::{ + admin::{delete_route, save_config, set_route, update_config, withdraw_support_funds}, + error::ContractError, + msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, + queries::{estimate_swap_result, SwapQuantity}, + state::{get_all_swap_routes, get_config, read_swap_route}, + swap::{handle_atomic_order_reply, start_swap_flow}, + types::{ConfigResponse, SwapQuantityMode}, }; -use cw2::{get_contract_version, set_contract_version}; -use crate::admin::{delete_route, save_config, set_route, update_config, withdraw_support_funds}; -use crate::helpers::handle_config_migration; -use crate::types::{ConfigResponse, SwapQuantityMode}; +use cosmwasm_std::{entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdError}; +use cw2::{get_contract_version, set_contract_version}; use injective_cosmwasm::{InjectiveMsgWrapper, InjectiveQueryWrapper}; -use crate::error::ContractError; - -use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; -use crate::queries::{estimate_swap_result, SwapQuantity}; -use crate::state::{get_all_swap_routes, get_config, read_swap_route}; -use crate::swap::{handle_atomic_order_reply, start_swap_flow}; - pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -33,9 +28,7 @@ pub fn instantiate( set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; save_config(deps, env, msg.admin, msg.fee_recipient)?; - Ok(Response::new() - .add_attribute("method", "instantiate") - .add_attribute("owner", info.sender)) + Ok(Response::new().add_attribute("method", "instantiate").add_attribute("owner", info.sender)) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -49,13 +42,7 @@ pub fn execute( ExecuteMsg::SwapMinOutput { target_denom, min_output_quantity, - } => start_swap_flow( - deps, - env, - info, - target_denom, - SwapQuantityMode::MinOutputQuantity(min_output_quantity), - ), + } => start_swap_flow(deps, env, info, target_denom, SwapQuantityMode::MinOutputQuantity(min_output_quantity)), ExecuteMsg::SwapExactOutput { target_denom, target_output_quantity, @@ -72,27 +59,14 @@ pub fn execute( target_denom, route, } => set_route(deps, &info.sender, source_denom, target_denom, route), - ExecuteMsg::DeleteRoute { - source_denom, - target_denom, - } => delete_route(deps, &info.sender, source_denom, target_denom), - ExecuteMsg::UpdateConfig { - admin, - fee_recipient, - } => update_config(deps, env, info.sender, admin, fee_recipient), - ExecuteMsg::WithdrawSupportFunds { - coins, - target_address, - } => withdraw_support_funds(deps, info.sender, coins, target_address), + ExecuteMsg::DeleteRoute { source_denom, target_denom } => delete_route(deps, &info.sender, source_denom, target_denom), + ExecuteMsg::UpdateConfig { admin, fee_recipient } => update_config(deps, env, info.sender, admin, fee_recipient), + ExecuteMsg::WithdrawSupportFunds { coins, target_address } => withdraw_support_funds(deps, info.sender, coins, target_address), } } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn reply( - deps: DepsMut, - env: Env, - msg: Reply, -) -> Result, ContractError> { +pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result, ContractError> { match msg.id { ATOMIC_ORDER_REPLY_ID => handle_atomic_order_reply(deps, env, msg), _ => Err(ContractError::UnrecognizedReply(msg.id)), @@ -100,95 +74,35 @@ pub fn reply( } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate( - deps: DepsMut, - _env: Env, - _msg: MigrateMsg, -) -> Result { - let contract_version = get_contract_version(deps.storage)?; - - match contract_version.contract.as_ref() { - // old contract name - "crates.io:atomic-order-example" => match contract_version.version.as_ref() { - "0.1.0" => { - unimplemented!( - "Migration from version {} is no longer supported", - contract_version.version - ); - } - "1.0.0" => { - set_contract_version( - deps.storage, - format!("crates.io:{CONTRACT_NAME}"), - CONTRACT_VERSION, - )?; - - handle_config_migration(deps)?; - } - _ => return Err(ContractError::MigrationError {}), - }, - "crates.io:swap-contract" => match contract_version.version.as_ref() { - "1.0.1" => { - unimplemented!( - "Migration from version {} is no yet supported", - contract_version.version - ); - } - _ => return Err(ContractError::MigrationError {}), - }, - _ => return Err(ContractError::MigrationError {}), - } - - Ok(Response::new() - .add_attribute("previous_contract_name", &contract_version.contract) - .add_attribute("previous_contract_version", &contract_version.version) - .add_attribute("new_contract_name", format!("crates.io:{CONTRACT_NAME}")) - .add_attribute("new_contract_version", CONTRACT_VERSION)) -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { match msg { - QueryMsg::GetRoute { - source_denom, - target_denom, - } => Ok(to_json_binary(&read_swap_route( - deps.storage, - &source_denom, - &target_denom, - )?)?), + QueryMsg::GetRoute { source_denom, target_denom } => to_json_binary(&read_swap_route(deps.storage, &source_denom, &target_denom)?), QueryMsg::GetOutputQuantity { from_quantity, source_denom, target_denom, - } => { - let target_quantity = estimate_swap_result( - deps, - &env, - source_denom, - target_denom, - SwapQuantity::InputQuantity(from_quantity), - )?; - Ok(to_json_binary(&target_quantity)?) - } + } => to_json_binary(&estimate_swap_result( + deps, + &env, + source_denom, + target_denom, + SwapQuantity::InputQuantity(from_quantity), + )?), + QueryMsg::GetInputQuantity { to_quantity, source_denom, target_denom, - } => { - let target_quantity = estimate_swap_result( - deps, - &env, - source_denom, - target_denom, - SwapQuantity::OutputQuantity(to_quantity), - )?; - Ok(to_json_binary(&target_quantity)?) - } - QueryMsg::GetAllRoutes {} => { - let routes = get_all_swap_routes(deps.storage)?; - Ok(to_json_binary(&routes)?) - } + } => to_json_binary(&estimate_swap_result( + deps, + &env, + source_denom, + target_denom, + SwapQuantity::OutputQuantity(to_quantity), + )?), + + QueryMsg::GetAllRoutes { start_after, limit } => to_json_binary(&get_all_swap_routes(deps.storage, start_after, limit)?), + QueryMsg::GetConfig {} => { let config = get_config(deps.storage)?; let config_response = ConfigResponse { @@ -199,3 +113,24 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdR } } } + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + let contract_version = get_contract_version(deps.storage)?; + + match contract_version.contract.as_ref() { + "crates.io:swap-contract" => match contract_version.version.as_ref() { + "1.0.1" => { + set_contract_version(deps.storage, format!("crates.io:{CONTRACT_NAME}"), CONTRACT_VERSION)?; + } + _ => return Err(ContractError::MigrationError {}), + }, + _ => return Err(ContractError::MigrationError {}), + } + + Ok(Response::new() + .add_attribute("previous_contract_name", &contract_version.contract) + .add_attribute("previous_contract_version", &contract_version.version) + .add_attribute("new_contract_name", format!("crates.io:{CONTRACT_NAME}")) + .add_attribute("new_contract_version", CONTRACT_VERSION)) +} diff --git a/contracts/swap/src/msg.rs b/contracts/swap/src/msg.rs index 37590b7..281c2f3 100644 --- a/contracts/swap/src/msg.rs +++ b/contracts/swap/src/msg.rs @@ -1,28 +1,24 @@ +use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Coin}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - use injective_cosmwasm::MarketId; use injective_math::FPDecimal; -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] +#[cw_serde] pub enum FeeRecipient { Address(Addr), SwapContract, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[cw_serde] pub struct InstantiateMsg { pub fee_recipient: FeeRecipient, pub admin: Addr, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[cw_serde] pub struct MigrateMsg {} -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] +#[cw_serde] pub enum ExecuteMsg { SwapMinOutput { target_denom: String, @@ -51,8 +47,7 @@ pub enum ExecuteMsg { }, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] +#[cw_serde] pub enum QueryMsg { GetRoute { source_denom: String, @@ -68,6 +63,9 @@ pub enum QueryMsg { source_denom: String, target_denom: String, }, - GetAllRoutes {}, + GetAllRoutes { + start_after: Option<(String, String)>, + limit: Option, + }, GetConfig {}, } diff --git a/contracts/swap/src/state.rs b/contracts/swap/src/state.rs index bcc85a2..bebcb95 100644 --- a/contracts/swap/src/state.rs +++ b/contracts/swap/src/state.rs @@ -1,14 +1,16 @@ -use cosmwasm_std::{Order, StdError, StdResult, Storage}; -use cw_storage_plus::{Item, Map}; - use crate::types::{Config, CurrentSwapOperation, CurrentSwapStep, SwapResults, SwapRoute}; +use cosmwasm_std::{Order, StdError, StdResult, Storage}; +use cw_storage_plus::{Bound, Item, Map}; + pub const SWAP_ROUTES: Map<(String, String), SwapRoute> = Map::new("swap_routes"); pub const SWAP_OPERATION_STATE: Item = Item::new("current_swap_cache"); pub const STEP_STATE: Item = Item::new("current_step_cache"); pub const SWAP_RESULTS: Item> = Item::new("swap_results"); pub const CONFIG: Item = Item::new("config"); +pub const DEFAULT_LIMIT: u32 = 100u32; + impl Config { pub fn validate(self) -> StdResult<()> { Ok(()) @@ -38,11 +40,22 @@ pub fn get_config(storage: &dyn Storage) -> StdResult { Ok(config) } -pub fn get_all_swap_routes(storage: &dyn Storage) -> StdResult> { +pub fn get_all_swap_routes( + storage: &dyn Storage, + start_after: Option<(String, String)>, + limit: Option, +) -> StdResult> { + let limit = limit.unwrap_or(DEFAULT_LIMIT) as usize; + + let start_bound = start_after + .as_ref() + .map(|(s, t)| Bound::inclusive((s.clone(), t.clone()))); + let routes = SWAP_ROUTES - .range(storage, None, None, Order::Ascending) - .map(|item| item.unwrap().1) - .collect(); + .range(storage, start_bound, None, Order::Ascending) + .take(limit) + .map(|item| item.map(|(_, route)| route)) // Extract the `SwapRoute` from each item + .collect::>>()?; Ok(routes) } diff --git a/contracts/swap/src/swap.rs b/contracts/swap/src/swap.rs index 7c3f2b3..0cb3256 100644 --- a/contracts/swap/src/swap.rs +++ b/contracts/swap/src/swap.rs @@ -1,28 +1,21 @@ -use std::str::FromStr; - -use cosmwasm_std::{ - BankMsg, Coin, DepsMut, Env, Event, MessageInfo, Reply, Response, StdResult, SubMsg, Uint128, +use crate::{ + contract::ATOMIC_ORDER_REPLY_ID, + error::ContractError, + helpers::{dec_scale_factor, round_up_to_min_tick}, + queries::{estimate_single_swap_execution, estimate_swap_result, SwapQuantity}, + state::{read_swap_route, CONFIG, STEP_STATE, SWAP_OPERATION_STATE, SWAP_RESULTS}, + types::{CurrentSwapOperation, CurrentSwapStep, FPCoin, SwapEstimationAmount, SwapQuantityMode, SwapResults}, }; -use protobuf::Message; - -use crate::contract::ATOMIC_ORDER_REPLY_ID; +use cosmwasm_std::{BankMsg, Coin, DepsMut, Env, Event, MessageInfo, Reply, Response, StdResult, SubMsg, Uint128}; use injective_cosmwasm::{ - create_spot_market_order_msg, get_default_subaccount_id_for_checked_address, - InjectiveMsgWrapper, InjectiveQuerier, InjectiveQueryWrapper, OrderType, SpotOrder, + create_spot_market_order_msg, get_default_subaccount_id_for_checked_address, InjectiveMsgWrapper, InjectiveQuerier, InjectiveQueryWrapper, + OrderType, SpotOrder, }; use injective_math::{round_to_min_tick, FPDecimal}; -use injective_protobuf::proto::tx; - -use crate::error::ContractError; -use crate::helpers::{dec_scale_factor, round_up_to_min_tick}; - -use crate::queries::{estimate_single_swap_execution, estimate_swap_result, SwapQuantity}; -use crate::state::{read_swap_route, CONFIG, STEP_STATE, SWAP_OPERATION_STATE, SWAP_RESULTS}; -use crate::types::{ - CurrentSwapOperation, CurrentSwapStep, FPCoin, SwapEstimationAmount, SwapQuantityMode, - SwapResults, -}; +use injective_std::types::injective::exchange::v1beta1::MsgCreateSpotMarketOrderResponse; +use prost::Message; +use std::str::FromStr; pub fn start_swap_flow( deps: DepsMut, @@ -56,10 +49,7 @@ pub fn start_swap_flow( let mut current_balance = coin_provided.to_owned().into(); - let refund_amount = if matches!( - swap_quantity_mode, - SwapQuantityMode::ExactOutputQuantity(..) - ) { + let refund_amount = if matches!(swap_quantity_mode, SwapQuantityMode::ExactOutputQuantity(..)) { let target_output_quantity = quantity; let estimation = estimate_swap_result( @@ -72,29 +62,20 @@ pub fn start_swap_flow( let querier = InjectiveQuerier::new(&deps.querier); let first_market_id = steps[0].to_owned(); - let first_market = querier - .query_spot_market(&first_market_id)? - .market - .expect("market should be available"); + let first_market = querier.query_spot_market(&first_market_id)?.market.expect("market should be available"); let is_input_quote = first_market.quote_denom == *source_denom; let required_input = if is_input_quote { estimation.result_quantity.int() + FPDecimal::ONE } else { - round_up_to_min_tick( - estimation.result_quantity, - first_market.min_quantity_tick_size, - ) + round_up_to_min_tick(estimation.result_quantity, first_market.min_quantity_tick_size) }; let fp_coins: FPDecimal = coin_provided.amount.into(); if required_input > fp_coins { - return Err(ContractError::InsufficientFundsProvided( - fp_coins, - required_input, - )); + return Err(ContractError::InsufficientFundsProvided(fp_coins, required_input)); } current_balance = FPCoin { @@ -111,7 +92,7 @@ pub fn start_swap_flow( sender_address, swap_steps: steps, swap_quantity_mode, - refund: Coin::new(refund_amount.into(), source_denom.to_owned()), + refund: Coin::new(refund_amount, source_denom.to_owned()), input_funds: coin_provided.to_owned(), }; @@ -160,10 +141,7 @@ pub fn execute_swap_step( None, ); - let order_message = SubMsg::reply_on_success( - create_spot_market_order_msg(contract.to_owned(), order), - ATOMIC_ORDER_REPLY_ID, - ); + let order_message = SubMsg::reply_on_success(create_spot_market_order_msg(contract.to_owned(), order), ATOMIC_ORDER_REPLY_ID); let current_step = CurrentSwapStep { step_idx, @@ -177,30 +155,12 @@ pub fn execute_swap_step( Ok(response) } -pub fn handle_atomic_order_reply( - deps: DepsMut, - env: Env, - msg: Reply, -) -> Result, ContractError> { +pub fn handle_atomic_order_reply(deps: DepsMut, env: Env, msg: Reply) -> Result, ContractError> { let dec_scale_factor = dec_scale_factor(); // protobuf serializes Dec values with extra 10^18 factor - let id = msg.id; - let order_response: tx::MsgCreateSpotMarketOrderResponse = Message::parse_from_bytes( - msg.result - .into_result() - .map_err(ContractError::SubMsgFailure)? - .data - .ok_or_else(|| ContractError::ReplyParseFailure { - id, - err: "Missing reply data".to_owned(), - })? - .as_slice(), - ) - .map_err(|err| ContractError::ReplyParseFailure { - id, - err: err.to_string(), - })?; - - let trade_data = match order_response.results.into_option() { + + let order_response = MsgCreateSpotMarketOrderResponse::decode(msg.payload.as_slice()).unwrap(); + + let trade_data = match order_response.results { Some(trade_data) => Ok(trade_data), None => Err(ContractError::CustomError { val: "No trade data in order response".to_string(), @@ -216,11 +176,7 @@ pub fn handle_atomic_order_reply( let current_step = STEP_STATE.load(deps.storage).map_err(ContractError::Std)?; - let new_quantity = if current_step.is_buy { - quantity - } else { - quantity * average_price - fee - }; + let new_quantity = if current_step.is_buy { quantity } else { quantity * average_price - fee }; let swap = SWAP_OPERATION_STATE.load(deps.storage)?; @@ -229,10 +185,7 @@ pub fn handle_atomic_order_reply( let new_rounded_quantity = if has_next_market { let querier = InjectiveQuerier::new(&deps.querier); let next_market_id = swap.swap_steps[(current_step.step_idx + 1) as usize].to_owned(); - let next_market = querier - .query_spot_market(&next_market_id)? - .market - .expect("market should be available"); + let next_market = querier.query_spot_market(&next_market_id)?.market.expect("market should be available"); let is_next_swap_sell = next_market.base_denom == current_step.step_target_denom; @@ -259,8 +212,7 @@ pub fn handle_atomic_order_reply( if current_step.step_idx < (swap.swap_steps.len() - 1) as u16 { SWAP_RESULTS.save(deps.storage, &swap_results)?; - return execute_swap_step(deps, env, swap, current_step.step_idx + 1, new_balance) - .map_err(ContractError::Std); + return execute_swap_step(deps, env, swap, current_step.step_idx + 1, new_balance).map_err(ContractError::Std); } let min_output_quantity = match swap.swap_quantity_mode { @@ -269,9 +221,7 @@ pub fn handle_atomic_order_reply( }; if new_balance.amount < min_output_quantity { - return Err(ContractError::MinOutputAmountNotReached( - min_output_quantity, - )); + return Err(ContractError::MinOutputAmountNotReached(min_output_quantity)); } // last step, finalize and send back funds to a caller @@ -294,9 +244,7 @@ pub fn handle_atomic_order_reply( STEP_STATE.remove(deps.storage); SWAP_RESULTS.remove(deps.storage); - let mut response = Response::new() - .add_message(send_message) - .add_event(swap_event); + let mut response = Response::new().add_message(send_message).add_event(swap_event); if swap.refund.amount > Uint128::zero() { let refund_message = BankMsg::Send { diff --git a/contracts/swap/src/testing/integration_logic_tests.rs b/contracts/swap/src/testing/integration_logic_tests.rs deleted file mode 100644 index d4f00a1..0000000 --- a/contracts/swap/src/testing/integration_logic_tests.rs +++ /dev/null @@ -1,2142 +0,0 @@ -use cosmwasm_std::{coin, Addr}; - -use injective_test_tube::RunnerError::{ExecuteError, QueryError}; -use injective_test_tube::{ - Account, Bank, Exchange, InjectiveTestApp, Module, RunnerError, RunnerResult, SigningAccount, - Wasm, -}; - -use injective_math::{round_to_min_tick, FPDecimal}; - -use crate::msg::{ExecuteMsg, QueryMsg}; -use crate::testing::test_utils::{ - are_fpdecimals_approximately_equal, assert_fee_is_as_expected, create_limit_order, - fund_account_with_some_inj, human_to_dec, init_contract_with_fee_recipient_and_get_address, - init_default_signer_account, init_default_validator_account, init_rich_account, - init_self_relaying_contract_and_get_address, launch_spot_market, must_init_account_with_funds, - pause_spot_market, query_all_bank_balances, query_bank_balance, set_route_and_assert_success, - str_coin, Decimals, OrderSide, ATOM, DEFAULT_ATOMIC_MULTIPLIER, DEFAULT_RELAYER_SHARE, - DEFAULT_SELF_RELAYING_FEE_PART, DEFAULT_TAKER_FEE, ETH, INJ, USDC, USDT, -}; -use crate::types::{FPCoin, SwapEstimationResult}; - -/* - This suite of tests focuses on calculation logic itself and doesn't attempt to use neither - realistic market configuration nor order prices, so that we don't have to deal with scaling issues. - - Hardcoded values used in these tests come from the first tab of this spreadsheet: - https://docs.google.com/spreadsheets/d/1-0epjX580nDO_P2mm1tSjhvjJVppsvrO1BC4_wsBeyA/edit?usp=sharing -*/ - -#[test] -fn it_executes_a_swap_between_two_base_assets_with_multiple_price_levels() { - let app = InjectiveTestApp::new(); - let wasm = Wasm::new(&app); - let exchange = Exchange::new(&app); - let bank = Bank::new(&app); - - let _signer = init_default_signer_account(&app); - let _validator = init_default_validator_account(&app); - let owner = init_rich_account(&app); - - let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); - let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); - - let contr_addr = init_self_relaying_contract_and_get_address( - &wasm, - &owner, - &[str_coin("100_000", USDT, Decimals::Six)], - ); - set_route_and_assert_success( - &wasm, - &owner, - &contr_addr, - ETH, - ATOM, - vec![ - spot_market_1_id.as_str().into(), - spot_market_2_id.as_str().into(), - ], - ); - - let trader1 = init_rich_account(&app); - let trader2 = init_rich_account(&app); - let trader3 = init_rich_account(&app); - - create_eth_buy_orders(&app, &spot_market_1_id, &trader1, &trader2); - create_atom_sell_orders(&app, &spot_market_2_id, &trader1, &trader2, &trader3); - - app.increase_time(1); - - let swapper = must_init_account_with_funds( - &app, - &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], - ); - - let mut query_result: SwapEstimationResult = wasm - .query( - &contr_addr, - &QueryMsg::GetOutputQuantity { - source_denom: ETH.to_string(), - target_denom: ATOM.to_string(), - from_quantity: FPDecimal::from(12u128), - }, - ) - .unwrap(); - - assert_eq!( - query_result.result_quantity, - FPDecimal::must_from_str("2893.886"), //slightly rounded down - "incorrect swap result estimate returned by query" - ); - - assert_eq!( - query_result.expected_fees.len(), - 2, - "Wrong number of fee denoms received" - ); - - let mut expected_fees = vec![ - FPCoin { - amount: FPDecimal::must_from_str("3541.5"), - denom: "usdt".to_string(), - }, - FPCoin { - amount: FPDecimal::must_from_str("3530.891412"), - denom: "usdt".to_string(), - }, - ]; - - assert_fee_is_as_expected( - &mut query_result.expected_fees, - &mut expected_fees, - FPDecimal::must_from_str("0.000001"), - ); - - let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); - - wasm.execute( - &contr_addr, - &ExecuteMsg::SwapMinOutput { - target_denom: ATOM.to_string(), - min_output_quantity: FPDecimal::from(2800u128), - }, - &[coin(12, ETH)], - &swapper, - ) - .unwrap(); - - let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); - let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); - assert_eq!( - from_balance, - FPDecimal::ZERO, - "some of the original amount wasn't swapped" - ); - assert_eq!( - to_balance, - FPDecimal::must_from_str("2893"), - "swapper did not receive expected amount" - ); - - let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); - assert_eq!( - contract_balances_after.len(), - 1, - "wrong number of denoms in contract balances" - ); - - let contract_balance_usdt_after = - FPDecimal::must_from_str(contract_balances_after[0].amount.as_str()); - let contract_balance_usdt_before = - FPDecimal::must_from_str(contract_balances_before[0].amount.as_str()); - - assert!( - contract_balance_usdt_after >= contract_balance_usdt_before, - "Contract lost some money after swap. Balance before: {contract_balance_usdt_before}, after: {contract_balance_usdt_after}", - ); - - let max_diff = human_to_dec("0.00001", Decimals::Six); - - assert!( - are_fpdecimals_approximately_equal( - contract_balance_usdt_after, - contract_balance_usdt_before, - max_diff, - ), - "Contract balance changed too much. Before: {}, After: {}", - contract_balances_before[0].amount, - contract_balances_after[0].amount - ); -} - -#[test] -fn it_executes_a_swap_between_two_base_assets_with_single_price_level() { - let app = InjectiveTestApp::new(); - let wasm = Wasm::new(&app); - let exchange = Exchange::new(&app); - let bank = Bank::new(&app); - - let _signer = init_default_signer_account(&app); - let _validator = init_default_validator_account(&app); - let owner = init_rich_account(&app); - - let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); - let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); - - let contr_addr = init_self_relaying_contract_and_get_address( - &wasm, - &owner, - &[str_coin("100_000", USDT, Decimals::Six)], - ); - set_route_and_assert_success( - &wasm, - &owner, - &contr_addr, - ETH, - ATOM, - vec![ - spot_market_1_id.as_str().into(), - spot_market_2_id.as_str().into(), - ], - ); - - let trader1 = init_rich_account(&app); - let trader2 = init_rich_account(&app); - let trader3 = init_rich_account(&app); - - create_eth_buy_orders(&app, &spot_market_1_id, &trader1, &trader2); - create_atom_sell_orders(&app, &spot_market_2_id, &trader1, &trader2, &trader3); - - app.increase_time(1); - - let swapper = must_init_account_with_funds( - &app, - &[coin(3, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], - ); - - let expected_atom_estimate_quantity = FPDecimal::must_from_str("751.492"); - let mut query_result: SwapEstimationResult = wasm - .query( - &contr_addr, - &QueryMsg::GetOutputQuantity { - source_denom: ETH.to_string(), - target_denom: ATOM.to_string(), - from_quantity: FPDecimal::from(3u128), - }, - ) - .unwrap(); - - assert_eq!( - query_result.result_quantity, expected_atom_estimate_quantity, - "incorrect swap result estimate returned by query" - ); - - let mut expected_fees = vec![ - FPCoin { - amount: FPDecimal::must_from_str("904.5"), - denom: "usdt".to_string(), - }, - FPCoin { - amount: FPDecimal::must_from_str("901.790564"), - denom: "usdt".to_string(), - }, - ]; - - assert_fee_is_as_expected( - &mut query_result.expected_fees, - &mut expected_fees, - human_to_dec("0.00001", Decimals::Six), - ); - - let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); - assert_eq!( - contract_balances_before.len(), - 1, - "wrong number of denoms in contract balances" - ); - - wasm.execute( - &contr_addr, - &ExecuteMsg::SwapMinOutput { - target_denom: ATOM.to_string(), - min_output_quantity: FPDecimal::from(750u128), - }, - &[coin(3, ETH)], - &swapper, - ) - .unwrap(); - - let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); - let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); - assert_eq!( - from_balance, - FPDecimal::ZERO, - "some of the original amount wasn't swapped" - ); - assert_eq!( - to_balance, - expected_atom_estimate_quantity.int(), - "swapper did not receive expected amount" - ); - - let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); - assert_eq!( - contract_balances_after.len(), - 1, - "wrong number of denoms in contract balances" - ); - assert_eq!( - contract_balances_after, contract_balances_before, - "contract balance has changed after swap" - ); -} - -#[test] -fn it_executes_swap_between_markets_using_different_quote_assets() { - let app = InjectiveTestApp::new(); - let wasm = Wasm::new(&app); - let exchange = Exchange::new(&app); - let bank = Bank::new(&app); - - let _signer = init_default_signer_account(&app); - let _validator = init_default_validator_account(&app); - let owner = init_rich_account(&app); - - let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); - let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDC); - let spot_market_3_id = launch_spot_market(&exchange, &owner, USDC, USDT); - - let contr_addr = init_self_relaying_contract_and_get_address( - &wasm, - &owner, - &[ - str_coin("100_000", USDC, Decimals::Six), - str_coin("100_000", USDT, Decimals::Six), - ], - ); - set_route_and_assert_success( - &wasm, - &owner, - &contr_addr, - ETH, - ATOM, - vec![ - spot_market_1_id.as_str().into(), - spot_market_3_id.as_str().into(), - spot_market_2_id.as_str().into(), - ], - ); - - let trader1 = init_rich_account(&app); - let trader2 = init_rich_account(&app); - let trader3 = init_rich_account(&app); - - create_eth_buy_orders(&app, &spot_market_1_id, &trader1, &trader2); - create_atom_sell_orders(&app, &spot_market_2_id, &trader1, &trader2, &trader3); - - //USDT-USDC - create_limit_order( - &app, - &trader3, - &spot_market_3_id, - OrderSide::Sell, - 1, - 100_000_000, - ); - - app.increase_time(1); - - let swapper = must_init_account_with_funds( - &app, - &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], - ); - - let mut query_result: SwapEstimationResult = wasm - .query( - &contr_addr, - &QueryMsg::GetOutputQuantity { - source_denom: ETH.to_string(), - target_denom: ATOM.to_string(), - from_quantity: FPDecimal::from(12u128), - }, - ) - .unwrap(); - - // expected amount is a bit lower, even though 1 USDT = 1 USDC, because of the fees - assert_eq!( - query_result.result_quantity, - FPDecimal::must_from_str("2889.64"), - "incorrect swap result estimate returned by query" - ); - - let mut expected_fees = vec![ - FPCoin { - amount: FPDecimal::must_from_str("3541.5"), - denom: "usdt".to_string(), - }, - FPCoin { - amount: FPDecimal::must_from_str("3530.891412"), - denom: "usdt".to_string(), - }, - FPCoin { - amount: FPDecimal::must_from_str("3525.603007"), - denom: "usdc".to_string(), - }, - ]; - - assert_fee_is_as_expected( - &mut query_result.expected_fees, - &mut expected_fees, - human_to_dec("0.000001", Decimals::Six), - ); - - let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); - assert_eq!( - contract_balances_before.len(), - 2, - "wrong number of denoms in contract balances" - ); - - wasm.execute( - &contr_addr, - &ExecuteMsg::SwapMinOutput { - target_denom: ATOM.to_string(), - min_output_quantity: FPDecimal::from(2800u128), - }, - &[coin(12, ETH)], - &swapper, - ) - .unwrap(); - - let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); - let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); - assert_eq!( - from_balance, - FPDecimal::ZERO, - "some of the original amount wasn't swapped" - ); - assert_eq!( - to_balance, - FPDecimal::must_from_str("2889"), - "swapper did not receive expected amount" - ); - - let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); - assert_eq!( - contract_balances_after.len(), - 2, - "wrong number of denoms in contract balances" - ); - assert_eq!( - contract_balances_after, contract_balances_before, - "contract balance has changed after swap" - ); -} - -#[test] -fn it_reverts_swap_between_markets_using_different_quote_asset_if_one_quote_buffer_is_insufficient() -{ - let app = InjectiveTestApp::new(); - let wasm = Wasm::new(&app); - let exchange = Exchange::new(&app); - let bank = Bank::new(&app); - - let _signer = init_default_signer_account(&app); - let _validator = init_default_validator_account(&app); - let owner = init_rich_account(&app); - - let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); - let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDC); - let spot_market_3_id = launch_spot_market(&exchange, &owner, USDC, USDT); - - let contr_addr = init_self_relaying_contract_and_get_address( - &wasm, - &owner, - &[ - str_coin("0.0001", USDC, Decimals::Six), - str_coin("100_000", USDT, Decimals::Six), - ], - ); - set_route_and_assert_success( - &wasm, - &owner, - &contr_addr, - ETH, - ATOM, - vec![ - spot_market_1_id.as_str().into(), - spot_market_3_id.as_str().into(), - spot_market_2_id.as_str().into(), - ], - ); - - let trader1 = init_rich_account(&app); - let trader2 = init_rich_account(&app); - let trader3 = init_rich_account(&app); - - create_eth_buy_orders(&app, &spot_market_1_id, &trader1, &trader2); - create_atom_sell_orders(&app, &spot_market_2_id, &trader1, &trader2, &trader3); - - //USDT-USDC - create_limit_order( - &app, - &trader3, - &spot_market_3_id, - OrderSide::Sell, - 1, - 100_000_000, - ); - - app.increase_time(1); - - let swapper = must_init_account_with_funds( - &app, - &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], - ); - - let query_result: RunnerResult = wasm.query( - &contr_addr, - &QueryMsg::GetOutputQuantity { - source_denom: ETH.to_string(), - target_denom: ATOM.to_string(), - from_quantity: FPDecimal::from(12u128), - }, - ); - - assert!(query_result.is_err(), "swap should have failed"); - assert!( - query_result - .unwrap_err() - .to_string() - .contains("Swap amount too high"), - "incorrect query result error message" - ); - - let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); - assert_eq!( - contract_balances_before.len(), - 2, - "wrong number of denoms in contract balances" - ); - - let execute_result = wasm.execute( - &contr_addr, - &ExecuteMsg::SwapMinOutput { - target_denom: ATOM.to_string(), - min_output_quantity: FPDecimal::from(2800u128), - }, - &[coin(12, ETH)], - &swapper, - ); - - assert!(execute_result.is_err(), "swap should have failed"); - assert!( - execute_result - .unwrap_err() - .to_string() - .contains("Swap amount too high"), - "incorrect query result error message" - ); - - let source_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); - let target_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); - assert_eq!( - source_balance, - FPDecimal::must_from_str("12"), - "source balance should not have changed after failed swap" - ); - assert_eq!( - target_balance, - FPDecimal::ZERO, - "target balance should not have changed after failed swap" - ); - - let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); - assert_eq!( - contract_balances_after.len(), - 2, - "wrong number of denoms in contract balances" - ); - assert_eq!( - contract_balances_after, contract_balances_before, - "contract balance has changed after swap" - ); -} - -#[test] -fn it_executes_a_sell_of_base_asset_to_receive_min_output_quantity() { - let app = InjectiveTestApp::new(); - let wasm = Wasm::new(&app); - let exchange = Exchange::new(&app); - let bank = Bank::new(&app); - - let _signer = init_default_signer_account(&app); - let _validator = init_default_validator_account(&app); - let owner = init_rich_account(&app); - - let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); - - let contr_addr = init_self_relaying_contract_and_get_address( - &wasm, - &owner, - &[str_coin("100_000", USDT, Decimals::Six)], - ); - set_route_and_assert_success( - &wasm, - &owner, - &contr_addr, - ETH, - USDT, - vec![spot_market_1_id.as_str().into()], - ); - - let trader1 = init_rich_account(&app); - let trader2 = init_rich_account(&app); - - create_eth_buy_orders(&app, &spot_market_1_id, &trader1, &trader2); - - app.increase_time(1); - - let swapper = must_init_account_with_funds( - &app, - &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], - ); - - let mut query_result: SwapEstimationResult = wasm - .query( - &contr_addr, - &QueryMsg::GetOutputQuantity { - source_denom: ETH.to_string(), - target_denom: USDT.to_string(), - from_quantity: FPDecimal::from(12u128), - }, - ) - .unwrap(); - - // calculate how much can be USDT can be bought for 12 ETH without fees - let orders_nominal_total_value = FPDecimal::from(201_000u128) * FPDecimal::from(5u128) - + FPDecimal::from(195_000u128) * FPDecimal::from(4u128) - + FPDecimal::from(192_000u128) * FPDecimal::from(3u128); - let expected_target_quantity = orders_nominal_total_value - * (FPDecimal::ONE - - FPDecimal::must_from_str(&format!( - "{}", - DEFAULT_TAKER_FEE * DEFAULT_ATOMIC_MULTIPLIER * DEFAULT_SELF_RELAYING_FEE_PART - ))); - - assert_eq!( - query_result.result_quantity, expected_target_quantity, - "incorrect swap result estimate returned by query" - ); - - let mut expected_fees = vec![FPCoin { - amount: FPDecimal::must_from_str("3541.5"), - denom: "usdt".to_string(), - }]; - - assert_fee_is_as_expected( - &mut query_result.expected_fees, - &mut expected_fees, - FPDecimal::must_from_str("0.000001"), - ); - - let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); - assert_eq!( - contract_balances_before.len(), - 1, - "wrong number of denoms in contract balances" - ); - - wasm.execute( - &contr_addr, - &ExecuteMsg::SwapMinOutput { - target_denom: USDT.to_string(), - min_output_quantity: FPDecimal::from(2357458u128), - }, - &[coin(12, ETH)], - &swapper, - ) - .unwrap(); - - let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); - let to_balance = query_bank_balance(&bank, USDT, swapper.address().as_str()); - let expected_execute_result = expected_target_quantity.int(); - - assert_eq!( - from_balance, - FPDecimal::ZERO, - "some of the original amount wasn't swapped" - ); - assert_eq!( - to_balance, expected_execute_result, - "swapper did not receive expected amount" - ); - - let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); - assert_eq!( - contract_balances_after.len(), - 1, - "wrong number of denoms in contract balances" - ); - assert_eq!( - contract_balances_after, contract_balances_before, - "contract balance has changed after swap" - ); -} - -#[test] -fn it_executes_a_buy_of_base_asset_to_receive_min_output_quantity() { - let app = InjectiveTestApp::new(); - let wasm = Wasm::new(&app); - let exchange = Exchange::new(&app); - let bank = Bank::new(&app); - - let _signer = init_default_signer_account(&app); - let _validator = init_default_validator_account(&app); - let owner = init_rich_account(&app); - - let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); - - let contr_addr = init_self_relaying_contract_and_get_address( - &wasm, - &owner, - &[str_coin("100_000", USDT, Decimals::Six)], - ); - set_route_and_assert_success( - &wasm, - &owner, - &contr_addr, - ETH, - USDT, - vec![spot_market_1_id.as_str().into()], - ); - - let trader1 = init_rich_account(&app); - let trader2 = init_rich_account(&app); - - create_limit_order( - &app, - &trader1, - &spot_market_1_id, - OrderSide::Sell, - 201_000, - 5, - ); - create_limit_order( - &app, - &trader2, - &spot_market_1_id, - OrderSide::Sell, - 195_000, - 4, - ); - create_limit_order( - &app, - &trader2, - &spot_market_1_id, - OrderSide::Sell, - 192_000, - 3, - ); - - app.increase_time(1); - - let swapper_usdt = 2_360_995; - let swapper = must_init_account_with_funds( - &app, - &[ - coin(swapper_usdt, USDT), - str_coin("500_000", INJ, Decimals::Eighteen), - ], - ); - - // calculate how much ETH we can buy with USDT we have - let available_usdt_after_fee = FPDecimal::from(swapper_usdt) - / (FPDecimal::ONE - + FPDecimal::must_from_str(&format!( - "{}", - DEFAULT_TAKER_FEE * DEFAULT_ATOMIC_MULTIPLIER * DEFAULT_SELF_RELAYING_FEE_PART - ))); - let usdt_left_for_most_expensive_order = available_usdt_after_fee - - (FPDecimal::from(195_000u128) * FPDecimal::from(4u128) - + FPDecimal::from(192_000u128) * FPDecimal::from(3u128)); - let most_expensive_order_quantity = - usdt_left_for_most_expensive_order / FPDecimal::from(201_000u128); - let expected_quantity = - most_expensive_order_quantity + (FPDecimal::from(4u128) + FPDecimal::from(3u128)); - - // round to min tick - let expected_quantity_rounded = - round_to_min_tick(expected_quantity, FPDecimal::must_from_str("0.001")); - - // calculate dust notional value as this will be the portion of user's funds that will stay in the contract - let dust = expected_quantity - expected_quantity_rounded; - // we need to use worst priced order - let dust_value = dust * FPDecimal::from(201_000u128); - - let mut query_result: SwapEstimationResult = wasm - .query( - &contr_addr, - &QueryMsg::GetOutputQuantity { - source_denom: USDT.to_string(), - target_denom: ETH.to_string(), - from_quantity: FPDecimal::from(swapper_usdt), - }, - ) - .unwrap(); - - assert_eq!( - query_result.result_quantity, expected_quantity_rounded, - "incorrect swap result estimate returned by query" - ); - - let mut expected_fees = vec![FPCoin { - amount: FPDecimal::must_from_str("3536.188217"), - denom: "usdt".to_string(), - }]; - - assert_fee_is_as_expected( - &mut query_result.expected_fees, - &mut expected_fees, - FPDecimal::must_from_str("0.000001"), - ); - - let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); - assert_eq!( - contract_balances_before.len(), - 1, - "wrong number of denoms in contract balances" - ); - - wasm.execute( - &contr_addr, - &ExecuteMsg::SwapMinOutput { - target_denom: ETH.to_string(), - min_output_quantity: FPDecimal::from(11u128), - }, - &[coin(swapper_usdt, USDT)], - &swapper, - ) - .unwrap(); - - let from_balance = query_bank_balance(&bank, USDT, swapper.address().as_str()); - let to_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); - let expected_execute_result = expected_quantity.int(); - - assert_eq!( - from_balance, - FPDecimal::ZERO, - "some of the original amount wasn't swapped" - ); - assert_eq!( - to_balance, expected_execute_result, - "swapper did not receive expected amount" - ); - - let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); - let mut expected_contract_balances_after = - FPDecimal::must_from_str(contract_balances_before[0].amount.as_str()) + dust_value; - expected_contract_balances_after = expected_contract_balances_after.int(); - - assert_eq!( - contract_balances_after.len(), - 1, - "wrong number of denoms in contract balances" - ); - assert_eq!( - FPDecimal::must_from_str(contract_balances_after[0].amount.as_str()), - expected_contract_balances_after, - "contract balance changed unexpectedly after swap" - ); -} - -#[test] -fn it_executes_a_swap_between_base_assets_with_external_fee_recipient() { - let app = InjectiveTestApp::new(); - let wasm = Wasm::new(&app); - let exchange = Exchange::new(&app); - let bank = Bank::new(&app); - - let _signer = init_default_signer_account(&app); - let _validator = init_default_validator_account(&app); - let owner = init_rich_account(&app); - - let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); - let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); - - let fee_recipient = must_init_account_with_funds(&app, &[]); - let contr_addr = init_contract_with_fee_recipient_and_get_address( - &wasm, - &owner, - &[str_coin("10_000", USDT, Decimals::Six)], - &fee_recipient, - ); - set_route_and_assert_success( - &wasm, - &owner, - &contr_addr, - ETH, - ATOM, - vec![ - spot_market_1_id.as_str().into(), - spot_market_2_id.as_str().into(), - ], - ); - - let trader1 = init_rich_account(&app); - let trader2 = init_rich_account(&app); - let trader3 = init_rich_account(&app); - - create_eth_buy_orders(&app, &spot_market_1_id, &trader1, &trader2); - create_atom_sell_orders(&app, &spot_market_2_id, &trader1, &trader2, &trader3); - - // calculate relayer's share of the fee based on assumptions that all orders are matched - let buy_orders_nominal_total_value = FPDecimal::from(201_000u128) * FPDecimal::from(5u128) - + FPDecimal::from(195_000u128) * FPDecimal::from(4u128) - + FPDecimal::from(192_000u128) * FPDecimal::from(3u128); - let relayer_sell_fee = buy_orders_nominal_total_value - * FPDecimal::must_from_str(&format!( - "{}", - DEFAULT_TAKER_FEE * DEFAULT_ATOMIC_MULTIPLIER * DEFAULT_RELAYER_SHARE - )); - - // calculate relayer's share of the fee based on assumptions that some of orders are matched - let expected_nominal_buy_most_expensive_match_quantity = - FPDecimal::must_from_str("488.2222155454736648"); - let sell_orders_nominal_total_value = FPDecimal::from(800u128) * FPDecimal::from(800u128) - + FPDecimal::from(810u128) * FPDecimal::from(800u128) - + FPDecimal::from(820u128) * FPDecimal::from(800u128) - + FPDecimal::from(830u128) * expected_nominal_buy_most_expensive_match_quantity; - let relayer_buy_fee = sell_orders_nominal_total_value - * FPDecimal::must_from_str(&format!( - "{}", - DEFAULT_TAKER_FEE * DEFAULT_ATOMIC_MULTIPLIER * DEFAULT_RELAYER_SHARE - )); - let expected_fee_for_fee_recipient = relayer_buy_fee + relayer_sell_fee; - - app.increase_time(1); - - let swapper = must_init_account_with_funds( - &app, - &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], - ); - - let mut query_result: SwapEstimationResult = wasm - .query( - &contr_addr, - &QueryMsg::GetOutputQuantity { - source_denom: ETH.to_string(), - target_denom: ATOM.to_string(), - from_quantity: FPDecimal::from(12u128), - }, - ) - .unwrap(); - - assert_eq!( - query_result.result_quantity, - FPDecimal::must_from_str("2888.221"), //slightly rounded down vs spreadsheet - "incorrect swap result estimate returned by query" - ); - - let mut expected_fees = vec![ - FPCoin { - amount: FPDecimal::must_from_str("5902.5"), - denom: "usdt".to_string(), - }, - FPCoin { - amount: FPDecimal::must_from_str("5873.061097"), - denom: "usdt".to_string(), - }, - ]; - - assert_fee_is_as_expected( - &mut query_result.expected_fees, - &mut expected_fees, - FPDecimal::must_from_str("0.000001"), - ); - - let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); - assert_eq!( - contract_balances_before.len(), - 1, - "wrong number of denoms in contract balances" - ); - - wasm.execute( - &contr_addr, - &ExecuteMsg::SwapMinOutput { - target_denom: ATOM.to_string(), - min_output_quantity: FPDecimal::from(2888u128), - }, - &[coin(12, ETH)], - &swapper, - ) - .unwrap(); - - let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); - let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); - - assert_eq!( - from_balance, - FPDecimal::ZERO, - "some of the original amount wasn't swapped" - ); - assert_eq!( - to_balance, - FPDecimal::must_from_str("2888"), - "swapper did not receive expected amount" - ); - - let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); - assert_eq!( - contract_balances_after.len(), - 1, - "wrong number of denoms in contract balances" - ); - - let contract_balance_usdt_after = - FPDecimal::must_from_str(contract_balances_after[0].amount.as_str()); - let contract_balance_usdt_before = - FPDecimal::must_from_str(contract_balances_before[0].amount.as_str()); - - assert!( - contract_balance_usdt_after >= contract_balance_usdt_before, - "Contract lost some money after swap. Balance before: {contract_balance_usdt_before}, after: {contract_balance_usdt_after}", - ); - - let max_diff = human_to_dec("0.00001", Decimals::Six); - - assert!( - are_fpdecimals_approximately_equal( - contract_balance_usdt_after, - contract_balance_usdt_before, - max_diff, - ), - "Contract balance changed too much. Before: {}, After: {}", - contract_balances_before[0].amount, - contract_balances_after[0].amount - ); - - let fee_recipient_balance = query_all_bank_balances(&bank, &fee_recipient.address()); - - assert_eq!( - fee_recipient_balance.len(), - 1, - "wrong number of denoms in fee recipient's balances" - ); - assert_eq!( - fee_recipient_balance[0].denom, USDT, - "fee recipient did not receive fee in expected denom" - ); - assert_eq!( - FPDecimal::must_from_str(fee_recipient_balance[0].amount.as_str()), - expected_fee_for_fee_recipient.int(), - "fee recipient did not receive expected fee" - ); -} - -#[test] -fn it_reverts_the_swap_if_there_isnt_enough_buffer_for_buying_target_asset() { - let app = InjectiveTestApp::new(); - let wasm = Wasm::new(&app); - let exchange = Exchange::new(&app); - let bank = Bank::new(&app); - - let _signer = init_default_signer_account(&app); - let _validator = init_default_validator_account(&app); - let owner = init_rich_account(&app); - - let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); - let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); - - let contr_addr = init_self_relaying_contract_and_get_address( - &wasm, - &owner, - &[str_coin("0.001", USDT, Decimals::Six)], - ); - set_route_and_assert_success( - &wasm, - &owner, - &contr_addr, - ETH, - ATOM, - vec![ - spot_market_1_id.as_str().into(), - spot_market_2_id.as_str().into(), - ], - ); - - let trader1 = init_rich_account(&app); - let trader2 = init_rich_account(&app); - let trader3 = init_rich_account(&app); - - create_eth_buy_orders(&app, &spot_market_1_id, &trader1, &trader2); - create_atom_sell_orders(&app, &spot_market_2_id, &trader1, &trader2, &trader3); - - app.increase_time(1); - - let swapper = must_init_account_with_funds( - &app, - &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], - ); - - let query_result: RunnerResult = wasm.query( - &contr_addr, - &QueryMsg::GetOutputQuantity { - source_denom: ETH.to_string(), - target_denom: ATOM.to_string(), - from_quantity: FPDecimal::from(12u128), - }, - ); - - assert!(query_result.is_err(), "query should fail"); - assert!( - query_result - .unwrap_err() - .to_string() - .contains("Swap amount too high"), - "wrong query error message" - ); - - let contract_balances_before = query_all_bank_balances(&bank, contr_addr.as_str()); - assert_eq!( - contract_balances_before.len(), - 1, - "wrong number of denoms in contract balances" - ); - - let execute_result = wasm.execute( - &contr_addr, - &ExecuteMsg::SwapMinOutput { - target_denom: ATOM.to_string(), - min_output_quantity: FPDecimal::from(2800u128), - }, - &[coin(12, ETH)], - &swapper, - ); - - assert!(execute_result.is_err(), "execute should fail"); - assert!( - execute_result - .unwrap_err() - .to_string() - .contains("Swap amount too high"), - "wrong execute error message" - ); - - let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); - let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); - assert_eq!( - from_balance, - FPDecimal::from(12u128), - "source balance changes after failed swap" - ); - assert_eq!( - to_balance, - FPDecimal::ZERO, - "target balance changes after failed swap" - ); - - let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); - assert_eq!( - contract_balances_after.len(), - 1, - "wrong number of denoms in contract balances" - ); - assert_eq!( - contract_balances_after, contract_balances_before, - "contract balance has changed after swap" - ); -} - -#[test] -fn it_reverts_swap_if_no_funds_were_passed() { - let app = InjectiveTestApp::new(); - let wasm = Wasm::new(&app); - let exchange = Exchange::new(&app); - let bank = Bank::new(&app); - - let _signer = init_default_signer_account(&app); - let _validator = init_default_validator_account(&app); - let owner = init_rich_account(&app); - - let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); - let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); - - let contr_addr = init_self_relaying_contract_and_get_address( - &wasm, - &owner, - &[str_coin("100_000", USDT, Decimals::Six)], - ); - set_route_and_assert_success( - &wasm, - &owner, - &contr_addr, - ETH, - ATOM, - vec![ - spot_market_1_id.as_str().into(), - spot_market_2_id.as_str().into(), - ], - ); - - let swapper = must_init_account_with_funds( - &app, - &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], - ); - - let contract_balances_before = query_all_bank_balances(&bank, contr_addr.as_str()); - assert_eq!( - contract_balances_before.len(), - 1, - "wrong number of denoms in contract balances" - ); - - let execute_result = wasm.execute( - &contr_addr, - &ExecuteMsg::SwapMinOutput { - target_denom: ATOM.to_string(), - min_output_quantity: FPDecimal::from(2800u128), - }, - &[], - &swapper, - ); - let expected_error = RunnerError::ExecuteError { msg: "failed to execute message; message index: 0: Custom Error: \"Only one denom can be passed in funds\": execute wasm contract failed".to_string() }; - assert_eq!( - execute_result.unwrap_err(), - expected_error, - "wrong error message" - ); - - let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); - let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); - - assert_eq!( - from_balance, - FPDecimal::from(12u128), - "source balance changes after failed swap" - ); - assert_eq!( - to_balance, - FPDecimal::ZERO, - "target balance changes after failed swap" - ); - - let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); - - assert_eq!( - contract_balances_after.len(), - 1, - "wrong number of denoms in contract balances" - ); - assert_eq!( - contract_balances_after, contract_balances_before, - "contract balance has changed after swap" - ); -} - -#[test] -fn it_reverts_swap_if_multiple_funds_were_passed() { - let app = InjectiveTestApp::new(); - let wasm = Wasm::new(&app); - let exchange = Exchange::new(&app); - let bank = Bank::new(&app); - - let _signer = init_default_signer_account(&app); - let _validator = init_default_validator_account(&app); - let owner = init_rich_account(&app); - - let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); - let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); - - let contr_addr = init_self_relaying_contract_and_get_address( - &wasm, - &owner, - &[str_coin("100_000", USDT, Decimals::Six)], - ); - set_route_and_assert_success( - &wasm, - &owner, - &contr_addr, - ETH, - ATOM, - vec![ - spot_market_1_id.as_str().into(), - spot_market_2_id.as_str().into(), - ], - ); - - let eth_balance = 12u128; - let atom_balance = 10u128; - - let swapper = must_init_account_with_funds( - &app, - &[ - coin(eth_balance, ETH), - coin(atom_balance, ATOM), - str_coin("500_000", INJ, Decimals::Eighteen), - ], - ); - - let contract_balances_before = query_all_bank_balances(&bank, contr_addr.as_str()); - assert_eq!( - contract_balances_before.len(), - 1, - "wrong number of denoms in contract balances" - ); - - let execute_result = wasm.execute( - &contr_addr, - &ExecuteMsg::SwapMinOutput { - target_denom: ATOM.to_string(), - min_output_quantity: FPDecimal::from(10u128), - }, - &[coin(10, ATOM), coin(12, ETH)], - &swapper, - ); - assert!( - execute_result - .unwrap_err() - .to_string() - .contains("Only one denom can be passed in funds"), - "wrong error message" - ); - - let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); - let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); - assert_eq!( - from_balance, - FPDecimal::from(eth_balance), - "wrong ETH balance after failed swap" - ); - assert_eq!( - to_balance, - FPDecimal::from(atom_balance), - "wrong ATOM balance after failed swap" - ); - - let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); - assert_eq!( - contract_balances_after.len(), - 1, - "wrong number of denoms in contract balances" - ); - assert_eq!( - contract_balances_after, contract_balances_before, - "contract balance has changed after swap" - ); -} - -#[test] -fn it_reverts_if_user_passes_quantities_equal_to_zero() { - let app = InjectiveTestApp::new(); - let wasm = Wasm::new(&app); - let exchange = Exchange::new(&app); - let bank = Bank::new(&app); - - let _signer = init_default_signer_account(&app); - let _validator = init_default_validator_account(&app); - let owner = init_rich_account(&app); - - let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); - let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); - - let contr_addr = init_self_relaying_contract_and_get_address( - &wasm, - &owner, - &[str_coin("100_000", USDT, Decimals::Six)], - ); - set_route_and_assert_success( - &wasm, - &owner, - &contr_addr, - ETH, - ATOM, - vec![ - spot_market_1_id.as_str().into(), - spot_market_2_id.as_str().into(), - ], - ); - - app.increase_time(1); - - let swapper = must_init_account_with_funds( - &app, - &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], - ); - - let query_result: RunnerResult = wasm.query( - &contr_addr, - &QueryMsg::GetOutputQuantity { - source_denom: ETH.to_string(), - target_denom: ATOM.to_string(), - from_quantity: FPDecimal::from(0u128), - }, - ); - assert!( - query_result - .unwrap_err() - .to_string() - .contains("source_quantity must be positive"), - "incorrect error returned by query" - ); - - let contract_balances_before = query_all_bank_balances(&bank, contr_addr.as_str()); - assert_eq!( - contract_balances_before.len(), - 1, - "wrong number of denoms in contract balances" - ); - - let err = wasm - .execute( - &contr_addr, - &ExecuteMsg::SwapMinOutput { - target_denom: ATOM.to_string(), - min_output_quantity: FPDecimal::ZERO, - }, - &[coin(12, ETH)], - &swapper, - ) - .unwrap_err(); - assert!( - err.to_string() - .contains("Output quantity must be positive!"), - "incorrect error returned by execute" - ); - - let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); - let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); - assert_eq!( - from_balance, - FPDecimal::must_from_str("12"), - "swap should not have occurred" - ); - assert_eq!( - to_balance, - FPDecimal::must_from_str("0"), - "swapper should not have received any target tokens" - ); - - let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); - assert_eq!( - contract_balances_after.len(), - 1, - "wrong number of denoms in contract balances" - ); - assert_eq!( - contract_balances_after, contract_balances_before, - "contract balance has changed after swap" - ); -} - -#[test] -fn it_reverts_if_user_passes_negative_quantities() { - let app = InjectiveTestApp::new(); - let wasm = Wasm::new(&app); - let exchange = Exchange::new(&app); - let bank = Bank::new(&app); - - let _signer = init_default_signer_account(&app); - let _validator = init_default_validator_account(&app); - let owner = init_rich_account(&app); - - let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); - let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); - - let contr_addr = init_self_relaying_contract_and_get_address( - &wasm, - &owner, - &[str_coin("100_000", USDT, Decimals::Six)], - ); - set_route_and_assert_success( - &wasm, - &owner, - &contr_addr, - ETH, - ATOM, - vec![ - spot_market_1_id.as_str().into(), - spot_market_2_id.as_str().into(), - ], - ); - - let contract_balances_before = query_all_bank_balances(&bank, contr_addr.as_str()); - assert_eq!( - contract_balances_before.len(), - 1, - "wrong number of denoms in contract balances" - ); - - let swapper = must_init_account_with_funds( - &app, - &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], - ); - - app.increase_time(1); - - let execute_result = wasm.execute( - &contr_addr, - &ExecuteMsg::SwapMinOutput { - target_denom: ATOM.to_string(), - min_output_quantity: FPDecimal::must_from_str("-1"), - }, - &[coin(12, ETH)], - &swapper, - ); - - assert!( - execute_result.is_err(), - "swap with negative minimum amount to receive did not fail" - ); - - let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); - let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); - assert_eq!( - from_balance, - FPDecimal::from(12u128), - "source balance changed after failed swap" - ); - assert_eq!( - to_balance, - FPDecimal::ZERO, - "target balance changed after failed swap" - ); - - let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); - assert_eq!( - contract_balances_after.len(), - 1, - "wrong number of denoms in contract balances" - ); - assert_eq!( - contract_balances_after, contract_balances_before, - "contract balance has changed after failed swap" - ); -} - -#[test] -fn it_reverts_if_there_arent_enough_orders_to_satisfy_min_quantity() { - let app = InjectiveTestApp::new(); - let wasm = Wasm::new(&app); - let exchange = Exchange::new(&app); - let bank = Bank::new(&app); - - let _signer = init_default_signer_account(&app); - let _validator = init_default_validator_account(&app); - let owner = init_rich_account(&app); - - let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); - let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); - - let contr_addr = init_self_relaying_contract_and_get_address( - &wasm, - &owner, - &[str_coin("100_000", USDT, Decimals::Six)], - ); - set_route_and_assert_success( - &wasm, - &owner, - &contr_addr, - ETH, - ATOM, - vec![ - spot_market_1_id.as_str().into(), - spot_market_2_id.as_str().into(), - ], - ); - - let trader1 = init_rich_account(&app); - let trader2 = init_rich_account(&app); - let trader3 = init_rich_account(&app); - - create_eth_buy_orders(&app, &spot_market_1_id, &trader1, &trader2); - - create_limit_order(&app, &trader1, &spot_market_2_id, OrderSide::Sell, 800, 800); - create_limit_order(&app, &trader2, &spot_market_2_id, OrderSide::Sell, 810, 800); - create_limit_order(&app, &trader3, &spot_market_2_id, OrderSide::Sell, 820, 800); - create_limit_order(&app, &trader1, &spot_market_2_id, OrderSide::Sell, 830, 450); //not enough for minimum requested - - app.increase_time(1); - - let swapper = must_init_account_with_funds( - &app, - &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], - ); - - let query_result: RunnerResult = wasm.query( - &contr_addr, - &QueryMsg::GetOutputQuantity { - source_denom: ETH.to_string(), - target_denom: ATOM.to_string(), - from_quantity: FPDecimal::from(12u128), - }, - ); - assert_eq!( - query_result.unwrap_err(), - QueryError { - msg: "Generic error: Not enough liquidity to fulfill order: query wasm contract failed" - .to_string() - }, - "wrong error message" - ); - - let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); - assert_eq!( - contract_balances_before.len(), - 1, - "wrong number of denoms in contract balances" - ); - - let execute_result = wasm.execute( - &contr_addr, - &ExecuteMsg::SwapMinOutput { - target_denom: ATOM.to_string(), - min_output_quantity: FPDecimal::from(2800u128), - }, - &[coin(12, ETH)], - &swapper, - ); - - assert_eq!(execute_result.unwrap_err(), RunnerError::ExecuteError { msg: "failed to execute message; message index: 0: dispatch: submessages: reply: Generic error: Not enough liquidity to fulfill order: execute wasm contract failed".to_string() }, "wrong error message"); - - let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); - let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); - assert_eq!( - from_balance, - FPDecimal::from(12u128), - "source balance changed after failed swap" - ); - assert_eq!( - to_balance, - FPDecimal::ZERO, - "target balance changed after failed swap" - ); - - let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); - assert_eq!( - contract_balances_after.len(), - 1, - "wrong number of denoms in contract balances" - ); - assert_eq!( - contract_balances_after, contract_balances_before, - "contract balance has changed after swap" - ); -} - -#[test] -fn it_reverts_if_min_quantity_cannot_be_reached() { - let app = InjectiveTestApp::new(); - let wasm = Wasm::new(&app); - let exchange = Exchange::new(&app); - let bank = Bank::new(&app); - - let _signer = init_default_signer_account(&app); - let _validator = init_default_validator_account(&app); - let owner = init_rich_account(&app); - - // set the market - let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); - let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); - - let contr_addr = init_self_relaying_contract_and_get_address( - &wasm, - &owner, - &[str_coin("100_000", USDT, Decimals::Six)], - ); - set_route_and_assert_success( - &wasm, - &owner, - &contr_addr, - ETH, - ATOM, - vec![ - spot_market_1_id.as_str().into(), - spot_market_2_id.as_str().into(), - ], - ); - - let trader1 = init_rich_account(&app); - let trader2 = init_rich_account(&app); - let trader3 = init_rich_account(&app); - - create_eth_buy_orders(&app, &spot_market_1_id, &trader1, &trader2); - create_atom_sell_orders(&app, &spot_market_2_id, &trader1, &trader2, &trader3); - - app.increase_time(1); - - let swapper = must_init_account_with_funds( - &app, - &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], - ); - - let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); - assert_eq!( - contract_balances_before.len(), - 1, - "wrong number of denoms in contract balances" - ); - - let min_quantity = 3500u128; - let execute_result = wasm.execute( - &contr_addr, - &ExecuteMsg::SwapMinOutput { - target_denom: ATOM.to_string(), - min_output_quantity: FPDecimal::from(min_quantity), - }, - &[coin(12, ETH)], - &swapper, - ); - - assert_eq!(execute_result.unwrap_err(), RunnerError::ExecuteError { msg: format!("failed to execute message; message index: 0: dispatch: submessages: reply: dispatch: submessages: reply: Min expected swap amount ({min_quantity}) not reached: execute wasm contract failed") }, "wrong error message"); - - let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); - let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); - assert_eq!( - from_balance, - FPDecimal::from(12u128), - "source balance changed after failed swap" - ); - assert_eq!( - to_balance, - FPDecimal::ZERO, - "target balance changed after failed swap" - ); - - let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); - assert_eq!( - contract_balances_after.len(), - 1, - "wrong number of denoms in contract balances" - ); - assert_eq!( - contract_balances_after, contract_balances_before, - "contract balance has changed after failed swap" - ); -} - -#[test] -fn it_reverts_if_market_is_paused() { - let app = InjectiveTestApp::new(); - let wasm = Wasm::new(&app); - let exchange = Exchange::new(&app); - let bank = Bank::new(&app); - - let signer = init_default_signer_account(&app); - let validator = init_default_validator_account(&app); - fund_account_with_some_inj(&bank, &signer, &validator); - let owner = init_rich_account(&app); - - let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); - let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); - - pause_spot_market(&app, spot_market_1_id.as_str(), &signer, &validator); - - let contr_addr = init_self_relaying_contract_and_get_address( - &wasm, - &owner, - &[str_coin("100_000", USDT, Decimals::Six)], - ); - set_route_and_assert_success( - &wasm, - &owner, - &contr_addr, - ETH, - ATOM, - vec![ - spot_market_1_id.as_str().into(), - spot_market_2_id.as_str().into(), - ], - ); - - let swapper = must_init_account_with_funds( - &app, - &[coin(12, ETH), str_coin("500_000", INJ, Decimals::Eighteen)], - ); - - let query_error: RunnerError = wasm - .query::( - &contr_addr, - &QueryMsg::GetOutputQuantity { - source_denom: ETH.to_string(), - target_denom: ATOM.to_string(), - from_quantity: FPDecimal::from(12u128), - }, - ) - .unwrap_err(); - - assert!( - query_error.to_string().contains("Querier contract error"), - "wrong error returned by query" - ); - - let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); - assert_eq!( - contract_balances_before.len(), - 1, - "wrong number of denoms in contract balances" - ); - - let execute_result = wasm.execute( - &contr_addr, - &ExecuteMsg::SwapMinOutput { - target_denom: ATOM.to_string(), - min_output_quantity: FPDecimal::from(2800u128), - }, - &[coin(12, ETH)], - &swapper, - ); - - assert!( - execute_result - .unwrap_err() - .to_string() - .contains("Querier contract error"), - "wrong error returned by execute" - ); - - let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); - let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); - assert_eq!( - from_balance, - FPDecimal::from(12u128), - "source balance changed after failed swap" - ); - assert_eq!( - to_balance, - FPDecimal::ZERO, - "target balance changed after failed swap" - ); - - let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); - assert_eq!( - contract_balances_after.len(), - 1, - "wrong number of denoms in contract balances" - ); - assert_eq!( - contract_balances_after, contract_balances_before, - "contract balance has changed after failed swap" - ); -} - -#[test] -fn it_reverts_if_user_doesnt_have_enough_inj_to_pay_for_gas() { - let app = InjectiveTestApp::new(); - let wasm = Wasm::new(&app); - let exchange = Exchange::new(&app); - let bank = Bank::new(&app); - - let _signer = init_default_signer_account(&app); - let _validator = init_default_validator_account(&app); - let owner = init_rich_account(&app); - - let spot_market_1_id = launch_spot_market(&exchange, &owner, ETH, USDT); - let spot_market_2_id = launch_spot_market(&exchange, &owner, ATOM, USDT); - - let contr_addr = init_self_relaying_contract_and_get_address( - &wasm, - &owner, - &[str_coin("100_000", USDT, Decimals::Six)], - ); - set_route_and_assert_success( - &wasm, - &owner, - &contr_addr, - ETH, - ATOM, - vec![ - spot_market_1_id.as_str().into(), - spot_market_2_id.as_str().into(), - ], - ); - - let swapper = must_init_account_with_funds(&app, &[coin(12, ETH), coin(10, INJ)]); - - let trader1 = init_rich_account(&app); - let trader2 = init_rich_account(&app); - let trader3 = init_rich_account(&app); - - create_eth_buy_orders(&app, &spot_market_1_id, &trader1, &trader2); - create_atom_sell_orders(&app, &spot_market_2_id, &trader1, &trader2, &trader3); - - app.increase_time(1); - - let query_result: RunnerResult = wasm.query( - &contr_addr, - &QueryMsg::GetOutputQuantity { - source_denom: ETH.to_string(), - target_denom: ATOM.to_string(), - from_quantity: FPDecimal::from(12u128), - }, - ); - - let target_quantity = query_result.unwrap().result_quantity; - - assert_eq!( - target_quantity, - FPDecimal::must_from_str("2893.886"), //slightly underestimated vs spreadsheet - "incorrect swap result estimate returned by query" - ); - - let contract_balances_before = query_all_bank_balances(&bank, &contr_addr); - assert_eq!( - contract_balances_before.len(), - 1, - "wrong number of denoms in contract balances" - ); - - let execute_result = wasm.execute( - &contr_addr, - &ExecuteMsg::SwapMinOutput { - target_denom: ATOM.to_string(), - min_output_quantity: FPDecimal::from(2800u128), - }, - &[coin(12, ETH)], - &swapper, - ); - - assert_eq!(execute_result.unwrap_err(), ExecuteError { msg: "spendable balance 10inj is smaller than 2500inj: insufficient funds: insufficient funds".to_string() }, "wrong error returned by execute"); - - let from_balance = query_bank_balance(&bank, ETH, swapper.address().as_str()); - let to_balance = query_bank_balance(&bank, ATOM, swapper.address().as_str()); - assert_eq!( - from_balance, - FPDecimal::from(12u128), - "source balance changed after failed swap" - ); - assert_eq!( - to_balance, - FPDecimal::ZERO, - "target balance changed after failed swap" - ); - - let contract_balances_after = query_all_bank_balances(&bank, contr_addr.as_str()); - assert_eq!( - contract_balances_after.len(), - 1, - "wrong number of denoms in contract balances" - ); - assert_eq!( - contract_balances_after, contract_balances_before, - "contract balance has changed after failed swap" - ); -} - -#[test] -fn it_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 deleted file mode 100644 index c3c3f7b..0000000 --- a/contracts/swap/src/testing/integration_realistic_tests_exact_quantity.rs +++ /dev/null @@ -1,1795 +0,0 @@ -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 deleted file mode 100644 index 6213738..0000000 --- a/contracts/swap/src/testing/integration_realistic_tests_min_quantity.rs +++ /dev/null @@ -1,1425 +0,0 @@ -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/queries_tests.rs b/contracts/swap/src/testing/queries_tests.rs index b7f6c2e..0363acc 100644 --- a/contracts/swap/src/testing/queries_tests.rs +++ b/contracts/swap/src/testing/queries_tests.rs @@ -574,7 +574,7 @@ fn get_all_queries_returns_empty_array_if_no_routes_are_set() { ) .unwrap(); - let all_routes_result = get_all_swap_routes(deps.as_ref().storage); + 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!( @@ -626,7 +626,7 @@ fn get_all_queries_returns_expected_array_if_routes_are_set() { ) .unwrap(); - let all_routes_result = get_all_swap_routes(deps.as_ref().storage); + let all_routes_result = get_all_swap_routes(deps.as_ref().storage, None, None); assert!(all_routes_result.is_ok(), "Error getting all routes"); let eth_inj_route = SwapRoute { diff --git a/contracts/swap/src/testing/test_utils.rs b/contracts/swap/src/testing/test_utils.rs index ccee9a3..dadc6ed 100644 --- a/contracts/swap/src/testing/test_utils.rs +++ b/contracts/swap/src/testing/test_utils.rs @@ -2,33 +2,23 @@ use std::collections::HashMap; use std::str::FromStr; use cosmwasm_std::testing::{MockApi, MockStorage}; -use cosmwasm_std::{ - coin, to_json_binary, Addr, Coin, ContractResult, OwnedDeps, QuerierResult, SystemError, - SystemResult, -}; +use cosmwasm_std::{coin, to_json_binary, Addr, Coin, ContractResult, OwnedDeps, QuerierResult, SystemError, SystemResult}; use injective_std::shim::Any; -use injective_std::types::cosmos::bank::v1beta1::{ - MsgSend, QueryAllBalancesRequest, QueryBalanceRequest, -}; +use injective_std::types::cosmos::bank::v1beta1::{MsgSend, QueryAllBalancesRequest, QueryBalanceRequest}; use injective_std::types::cosmos::base::v1beta1::Coin as TubeCoin; use injective_std::types::cosmos::gov::v1::MsgVote; use injective_std::types::cosmos::gov::v1beta1::MsgSubmitProposal; use injective_std::types::injective::exchange; use injective_std::types::injective::exchange::v1beta1::{ - MsgCreateSpotLimitOrder, MsgInstantSpotMarketLaunch, OrderInfo, OrderType, - QuerySpotMarketsRequest, SpotMarketParamUpdateProposal, SpotOrder, -}; -use injective_test_tube::{ - Account, Bank, Exchange, Gov, InjectiveTestApp, Module, SigningAccount, Wasm, + MsgCreateSpotLimitOrder, MsgInstantSpotMarketLaunch, OrderInfo, OrderType, QuerySpotMarketsRequest, SpotMarketParamUpdateProposal, SpotOrder, }; +use injective_test_tube::{Account, Bank, Exchange, Gov, InjectiveTestApp, Module, SigningAccount, Wasm}; use crate::helpers::Scaled; 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, WasmMockQuerier, TEST_MARKET_ID_1, - TEST_MARKET_ID_2, + 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, + WasmMockQuerier, TEST_MARKET_ID_1, TEST_MARKET_ID_2, }; use injective_math::FPDecimal; use prost::Message; @@ -89,21 +79,11 @@ pub fn mock_deps_eth_inj( let mut markets = HashMap::new(); markets.insert( MarketId::new(TEST_MARKET_ID_1).unwrap(), - create_mock_spot_market( - "eth", - FPDecimal::must_from_str("0.001"), - FPDecimal::must_from_str("0.001"), - 0, - ), + create_mock_spot_market("eth", FPDecimal::must_from_str("0.001"), FPDecimal::must_from_str("0.001"), 0), ); markets.insert( MarketId::new(TEST_MARKET_ID_2).unwrap(), - create_mock_spot_market( - "inj", - FPDecimal::must_from_str("0.001"), - FPDecimal::must_from_str("0.001"), - 1, - ), + create_mock_spot_market("inj", FPDecimal::must_from_str("0.001"), FPDecimal::must_from_str("0.001"), 1), ); querier.spot_market_response_handler = create_spot_multi_market_handler(markets); @@ -144,8 +124,7 @@ pub fn mock_deps_eth_inj( ]; orderbooks.insert(MarketId::new(TEST_MARKET_ID_2).unwrap(), inj_sell_orderbook); - querier.spot_market_orderbook_response_handler = - create_orderbook_response_handler(orderbooks); + querier.spot_market_orderbook_response_handler = create_orderbook_response_handler(orderbooks); if multiplier_query_behavior == MultiplierQueryBehavior::Fail { pub fn create_spot_error_multiplier_handler() -> Option> { @@ -160,8 +139,7 @@ pub fn mock_deps_eth_inj( Some(Box::new(Temp {})) } - querier.market_atomic_execution_fee_multiplier_response_handler = - create_spot_error_multiplier_handler() + querier.market_atomic_execution_fee_multiplier_response_handler = create_spot_error_multiplier_handler() } else { pub fn create_spot_ok_multiplier_handler() -> Option> { struct Temp {} @@ -178,8 +156,7 @@ pub fn mock_deps_eth_inj( Some(Box::new(Temp {})) } - querier.market_atomic_execution_fee_multiplier_response_handler = - create_spot_ok_multiplier_handler() + querier.market_atomic_execution_fee_multiplier_response_handler = create_spot_ok_multiplier_handler() } }) } @@ -200,12 +177,7 @@ pub fn mock_realistic_deps_eth_atom( ); markets.insert( MarketId::new(TEST_MARKET_ID_2).unwrap(), - create_mock_spot_market( - "atom", - FPDecimal::must_from_str("0.001"), - FPDecimal::must_from_str("10000"), - 1, - ), + create_mock_spot_market("atom", FPDecimal::must_from_str("0.001"), FPDecimal::must_from_str("10000"), 1), ); querier.spot_market_response_handler = create_spot_multi_market_handler(markets); @@ -246,8 +218,7 @@ pub fn mock_realistic_deps_eth_atom( ]; orderbooks.insert(MarketId::new(TEST_MARKET_ID_2).unwrap(), inj_sell_orderbook); - querier.spot_market_orderbook_response_handler = - create_orderbook_response_handler(orderbooks); + querier.spot_market_orderbook_response_handler = create_orderbook_response_handler(orderbooks); if multiplier_query_behavior == MultiplierQueryBehavior::Fail { pub fn create_spot_error_multiplier_handler() -> Option> { @@ -262,8 +233,7 @@ pub fn mock_realistic_deps_eth_atom( Some(Box::new(Temp {})) } - querier.market_atomic_execution_fee_multiplier_response_handler = - create_spot_error_multiplier_handler() + querier.market_atomic_execution_fee_multiplier_response_handler = create_spot_error_multiplier_handler() } else { pub fn create_spot_ok_multiplier_handler() -> Option> { struct Temp {} @@ -280,18 +250,12 @@ pub fn mock_realistic_deps_eth_atom( Some(Box::new(Temp {})) } - querier.market_atomic_execution_fee_multiplier_response_handler = - create_spot_ok_multiplier_handler() + querier.market_atomic_execution_fee_multiplier_response_handler = create_spot_ok_multiplier_handler() } }) } -fn create_mock_spot_market( - base: &str, - min_price_tick_size: FPDecimal, - min_quantity_tick_size: FPDecimal, - idx: u32, -) -> SpotMarket { +fn create_mock_spot_market(base: &str, min_price_tick_size: FPDecimal, min_quantity_tick_size: FPDecimal, idx: u32) -> SpotMarket { SpotMarket { ticker: format!("{base}usdt"), base_denom: base.to_string(), @@ -303,35 +267,23 @@ fn create_mock_spot_market( status: injective_cosmwasm::MarketStatus::Active, min_price_tick_size, min_quantity_tick_size, + min_notional: "0".to_string(), } } pub fn wasm_file(contract_name: String) -> String { let arch = std::env::consts::ARCH; - let artifacts_dir = - std::env::var("ARTIFACTS_DIR_PATH").unwrap_or_else(|_| "artifacts".to_string()); + let artifacts_dir = std::env::var("ARTIFACTS_DIR_PATH").unwrap_or_else(|_| "artifacts".to_string()); let snaked_name = contract_name.replace('-', "_"); format!("../../{artifacts_dir}/{snaked_name}-{arch}.wasm") } -pub fn store_code( - wasm: &Wasm, - owner: &SigningAccount, - contract_name: String, -) -> u64 { +pub fn store_code(wasm: &Wasm, owner: &SigningAccount, contract_name: String) -> u64 { let wasm_byte_code = std::fs::read(wasm_file(contract_name)).unwrap(); - wasm.store_code(&wasm_byte_code, None, owner) - .unwrap() - .data - .code_id + wasm.store_code(&wasm_byte_code, None, owner).unwrap().data.code_id } -pub fn launch_spot_market( - exchange: &Exchange, - signer: &SigningAccount, - base: &str, - quote: &str, -) -> 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( @@ -342,6 +294,7 @@ pub fn launch_spot_market( 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, ) @@ -368,6 +321,7 @@ pub fn launch_custom_spot_market( 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, ) @@ -390,10 +344,7 @@ pub fn get_spot_market_id(exchange: &Exchange, ticker: String) market.market_id.to_string() } -pub fn launch_realistic_inj_usdt_spot_market( - exchange: &Exchange, - signer: &SigningAccount, -) -> String { +pub fn launch_realistic_inj_usdt_spot_market(exchange: &Exchange, signer: &SigningAccount) -> String { launch_custom_spot_market( exchange, signer, @@ -404,10 +355,7 @@ pub fn launch_realistic_inj_usdt_spot_market( ) } -pub fn launch_realistic_weth_usdt_spot_market( - exchange: &Exchange, - signer: &SigningAccount, -) -> String { +pub fn launch_realistic_weth_usdt_spot_market(exchange: &Exchange, signer: &SigningAccount) -> String { launch_custom_spot_market( exchange, signer, @@ -418,10 +366,7 @@ pub fn launch_realistic_weth_usdt_spot_market( ) } -pub fn launch_realistic_atom_usdt_spot_market( - exchange: &Exchange, - signer: &SigningAccount, -) -> String { +pub fn launch_realistic_atom_usdt_spot_market(exchange: &Exchange, signer: &SigningAccount) -> String { launch_custom_spot_market( exchange, signer, @@ -432,10 +377,7 @@ pub fn launch_realistic_atom_usdt_spot_market( ) } -pub fn launch_realistic_usdt_usdc_spot_market( - exchange: &Exchange, - signer: &SigningAccount, -) -> String { +pub fn launch_realistic_usdt_usdc_spot_market(exchange: &Exchange, signer: &SigningAccount) -> String { launch_custom_spot_market( exchange, signer, @@ -446,10 +388,7 @@ pub fn launch_realistic_usdt_usdc_spot_market( ) } -pub fn launch_realistic_ninja_inj_spot_market( - exchange: &Exchange, - signer: &SigningAccount, -) -> String { +pub fn launch_realistic_ninja_inj_spot_market(exchange: &Exchange, signer: &SigningAccount) -> String { launch_custom_spot_market( exchange, signer, @@ -477,16 +416,7 @@ pub fn create_realistic_eth_usdt_buy_orders_from_spreadsheet( Decimals::Six, ); - create_realistic_limit_order( - app, - trader2, - market_id, - OrderSide::Buy, - "1978", - "1.23", - Decimals::Eighteen, - Decimals::Six, - ); + create_realistic_limit_order(app, trader2, market_id, OrderSide::Buy, "1978", "1.23", Decimals::Eighteen, Decimals::Six); create_realistic_limit_order( app, @@ -592,11 +522,7 @@ pub fn create_realistic_inj_usdt_buy_orders_from_spreadsheet( ); } -pub fn create_realistic_inj_usdt_sell_orders_from_spreadsheet( - app: &InjectiveTestApp, - market_id: &str, - trader1: &SigningAccount, -) { +pub fn create_realistic_inj_usdt_sell_orders_from_spreadsheet(app: &InjectiveTestApp, market_id: &str, trader1: &SigningAccount) { create_realistic_limit_order( app, trader1, @@ -616,56 +542,16 @@ pub fn create_realistic_atom_usdt_sell_orders_from_spreadsheet( trader2: &SigningAccount, trader3: &SigningAccount, ) { - create_realistic_limit_order( - app, - trader1, - market_id, - OrderSide::Sell, - "8.89", - "197.89", - Decimals::Six, - Decimals::Six, - ); + create_realistic_limit_order(app, trader1, market_id, OrderSide::Sell, "8.89", "197.89", Decimals::Six, Decimals::Six); - create_realistic_limit_order( - app, - trader2, - market_id, - OrderSide::Sell, - "8.93", - "181.02", - Decimals::Six, - Decimals::Six, - ); + create_realistic_limit_order(app, trader2, market_id, OrderSide::Sell, "8.93", "181.02", Decimals::Six, Decimals::Six); - create_realistic_limit_order( - app, - trader3, - market_id, - OrderSide::Sell, - "8.99", - "203.12", - Decimals::Six, - Decimals::Six, - ); + create_realistic_limit_order(app, trader3, market_id, OrderSide::Sell, "8.99", "203.12", Decimals::Six, Decimals::Six); - create_realistic_limit_order( - app, - trader1, - market_id, - OrderSide::Sell, - "9.01", - "421.11", - Decimals::Six, - Decimals::Six, - ); + create_realistic_limit_order(app, trader1, market_id, OrderSide::Sell, "9.01", "421.11", Decimals::Six, Decimals::Six); } -pub fn create_realistic_usdt_usdc_both_side_orders( - app: &InjectiveTestApp, - market_id: &str, - trader1: &SigningAccount, -) { +pub fn create_realistic_usdt_usdc_both_side_orders(app: &InjectiveTestApp, market_id: &str, trader1: &SigningAccount) { create_realistic_limit_order( app, trader1, @@ -690,11 +576,7 @@ pub fn create_realistic_usdt_usdc_both_side_orders( } // not really realistic yet -pub fn create_ninja_inj_both_side_orders( - app: &InjectiveTestApp, - market_id: &str, - trader1: &SigningAccount, -) { +pub fn create_ninja_inj_both_side_orders(app: &InjectiveTestApp, market_id: &str, trader1: &SigningAccount) { create_realistic_limit_order( app, trader1, @@ -713,14 +595,7 @@ pub enum OrderSide { Sell, } -pub fn create_limit_order( - app: &InjectiveTestApp, - trader: &SigningAccount, - market_id: &str, - order_side: OrderSide, - price: u128, - quantity: u32, -) { +pub fn create_limit_order(app: &InjectiveTestApp, trader: &SigningAccount, market_id: &str, order_side: OrderSide, price: u128, quantity: u32) { let exchange = Exchange::new(app); exchange .create_spot_limit_order( @@ -729,13 +604,11 @@ pub fn create_limit_order( order: Some(SpotOrder { market_id: market_id.to_string(), order_info: Some(OrderInfo { - subaccount_id: get_default_subaccount_id_for_checked_address( - &Addr::unchecked(trader.address()), - ) - .to_string(), + subaccount_id: get_default_subaccount_id_for_checked_address(&Addr::unchecked(trader.address())).to_string(), fee_recipient: trader.address(), price: format!("{price}000000000000000000"), quantity: format!("{quantity}000000000000000000"), + cid: "".to_string(), }), order_type: if order_side == OrderSide::Buy { OrderType::BuyAtomic.into() @@ -750,17 +623,11 @@ pub fn create_limit_order( .unwrap(); } -pub fn scale_price_quantity_for_market( - price: &str, - quantity: &str, - base_decimals: &Decimals, - quote_decimals: &Decimals, -) -> (String, String) { +pub fn scale_price_quantity_for_market(price: &str, quantity: &str, base_decimals: &Decimals, quote_decimals: &Decimals) -> (String, String) { let price_dec = FPDecimal::must_from_str(price.replace('_', "").as_str()); let quantity_dec = FPDecimal::must_from_str(quantity.replace('_', "").as_str()); - let scaled_price = - price_dec.scaled(quote_decimals.get_decimals() - base_decimals.get_decimals()); + let scaled_price = price_dec.scaled(quote_decimals.get_decimals() - base_decimals.get_decimals()); let scaled_quantity = quantity_dec.scaled(base_decimals.get_decimals()); (dec_to_proto(scaled_price), dec_to_proto(scaled_quantity)) } @@ -780,8 +647,7 @@ pub fn create_realistic_limit_order( base_decimals: Decimals, quote_decimals: Decimals, ) { - let (price_to_send, quantity_to_send) = - scale_price_quantity_for_market(price, quantity, &base_decimals, "e_decimals); + let (price_to_send, quantity_to_send) = scale_price_quantity_for_market(price, quantity, &base_decimals, "e_decimals); let exchange = Exchange::new(app); exchange @@ -791,13 +657,11 @@ pub fn create_realistic_limit_order( order: Some(SpotOrder { market_id: market_id.to_string(), order_info: Some(OrderInfo { - subaccount_id: get_default_subaccount_id_for_checked_address( - &Addr::unchecked(trader.address()), - ) - .to_string(), + subaccount_id: get_default_subaccount_id_for_checked_address(&Addr::unchecked(trader.address())).to_string(), fee_recipient: trader.address(), price: price_to_send, quantity: quantity_to_send, + cid: "".to_string(), }), order_type: if order_side == OrderSide::Buy { OrderType::BuyAtomic.into() @@ -812,11 +676,7 @@ pub fn create_realistic_limit_order( .unwrap(); } -pub fn init_self_relaying_contract_and_get_address( - wasm: &Wasm, - owner: &SigningAccount, - initial_balance: &[Coin], -) -> String { +pub fn init_self_relaying_contract_and_get_address(wasm: &Wasm, owner: &SigningAccount, initial_balance: &[Coin]) -> String { let code_id = store_code(wasm, owner, "swap_contract".to_string()); wasm.instantiate( code_id, @@ -878,19 +738,14 @@ pub fn set_route_and_assert_success( .unwrap(); } -pub fn must_init_account_with_funds( - app: &InjectiveTestApp, - initial_funds: &[Coin], -) -> SigningAccount { +pub fn must_init_account_with_funds(app: &InjectiveTestApp, initial_funds: &[Coin]) -> SigningAccount { app.init_account(initial_funds).unwrap() } -pub fn query_all_bank_balances( - bank: &Bank, - address: &str, -) -> Vec { +pub fn query_all_bank_balances(bank: &Bank, address: &str) -> Vec { bank.query_all_balances(&QueryAllBalancesRequest { address: address.to_string(), + resolve_denom: false, pagination: None, }) .unwrap() @@ -912,12 +767,7 @@ pub fn query_bank_balance(bank: &Bank, denom: &str, address: & .unwrap() } -pub fn pause_spot_market( - app: &InjectiveTestApp, - market_id: &str, - proposer: &SigningAccount, - validator: &SigningAccount, -) { +pub fn pause_spot_market(app: &InjectiveTestApp, market_id: &str, proposer: &SigningAccount, validator: &SigningAccount) { let gov = Gov::new(app); pass_spot_market_params_update_proposal( &gov, @@ -931,6 +781,9 @@ pub fn pause_spot_market( min_price_tick_size: "".to_string(), min_quantity_tick_size: "".to_string(), status: 2, + min_notional: "0".to_string(), + ticker: "0".to_string(), + admin_info: "0".to_string(), }, proposer, validator, @@ -982,8 +835,7 @@ pub fn pass_spot_market_params_update_proposal( } pub fn init_default_validator_account(app: &InjectiveTestApp) -> SigningAccount { - app.get_first_validator_signing_account(INJ.to_string(), 1.2f64) - .unwrap() + app.get_first_validator_signing_account(INJ.to_string(), 1.2f64).unwrap() } pub fn init_default_signer_account(app: &InjectiveTestApp) -> SigningAccount { @@ -1005,11 +857,7 @@ pub fn init_rich_account(app: &InjectiveTestApp) -> SigningAccount { ) } -pub fn fund_account_with_some_inj( - bank: &Bank, - from: &SigningAccount, - to: &SigningAccount, -) { +pub fn fund_account_with_some_inj(bank: &Bank, from: &SigningAccount, to: &SigningAccount) { bank.send( MsgSend { from_address: from.address(), @@ -1029,9 +877,7 @@ pub fn human_to_dec(raw_number: &str, decimals: Decimals) -> FPDecimal { } pub fn human_to_proto(raw_number: &str, decimals: i32) -> String { - FPDecimal::must_from_str(&raw_number.replace('_', "")) - .scaled(18 + decimals) - .to_string() + FPDecimal::must_from_str(&raw_number.replace('_', "")).scaled(18 + decimals).to_string() } pub fn str_coin(human_amount: &str, denom: &str, decimals: Decimals) -> Coin { @@ -1041,9 +887,7 @@ pub fn str_coin(human_amount: &str, denom: &str, decimals: Decimals) -> Coin { } mod tests { - use crate::testing::test_utils::{ - human_to_dec, human_to_proto, scale_price_quantity_for_market, Decimals, - }; + use crate::testing::test_utils::{human_to_dec, human_to_proto, scale_price_quantity_for_market, Decimals}; use injective_math::FPDecimal; #[test] @@ -1053,19 +897,13 @@ mod tests { let mut expected = FPDecimal::must_from_str("1000000000000000000"); let actual = human_to_dec(integer, decimals); - assert_eq!( - actual, expected, - "failed to convert integer with 18 decimal to dec" - ); + assert_eq!(actual, expected, "failed to convert integer with 18 decimal to dec"); decimals = Decimals::Six; expected = FPDecimal::must_from_str("1000000"); let actual = human_to_dec(integer, decimals); - assert_eq!( - actual, expected, - "failed to convert integer with 6 decimal to dec" - ); + assert_eq!(actual, expected, "failed to convert integer with 6 decimal to dec"); } #[test] @@ -1075,19 +913,13 @@ mod tests { let mut expected = FPDecimal::must_from_str("1100000000000000000"); let actual = human_to_dec(integer, decimals); - assert_eq!( - actual, expected, - "failed to convert integer with 18 decimal to dec" - ); + assert_eq!(actual, expected, "failed to convert integer with 18 decimal to dec"); decimals = Decimals::Six; expected = FPDecimal::must_from_str("1100000"); let actual = human_to_dec(integer, decimals); - assert_eq!( - actual, expected, - "failed to convert integer with 6 decimal to dec" - ); + assert_eq!(actual, expected, "failed to convert integer with 6 decimal to dec"); } #[test] @@ -1097,10 +929,7 @@ mod tests { let expected = FPDecimal::must_from_str("1000000000000000001"); let actual = human_to_dec(integer, decimals); - assert_eq!( - actual, expected, - "failed to convert integer with 18 decimal to dec" - ); + assert_eq!(actual, expected, "failed to convert integer with 18 decimal to dec"); } #[test] @@ -1110,10 +939,7 @@ mod tests { let expected = FPDecimal::must_from_str("1000001"); let actual = human_to_dec(integer, decimals); - assert_eq!( - actual, expected, - "failed to convert integer with 18 decimal to dec" - ); + assert_eq!(actual, expected, "failed to convert integer with 18 decimal to dec"); } #[test] @@ -1123,19 +949,13 @@ mod tests { let mut expected = FPDecimal::must_from_str("112300000000000000"); let actual = human_to_dec(integer, decimals); - assert_eq!( - actual, expected, - "failed to convert integer with 18 decimal to dec" - ); + assert_eq!(actual, expected, "failed to convert integer with 18 decimal to dec"); decimals = Decimals::Six; expected = FPDecimal::must_from_str("112300"); let actual = human_to_dec(integer, decimals); - assert_eq!( - actual, expected, - "failed to convert integer with 6 decimal to dec" - ); + assert_eq!(actual, expected, "failed to convert integer with 6 decimal to dec"); } #[test] @@ -1145,10 +965,7 @@ mod tests { let expected = FPDecimal::must_from_str("1"); let actual = human_to_dec(integer, decimals); - assert_eq!( - actual, expected, - "failed to convert integer with 18 decimal to dec" - ); + assert_eq!(actual, expected, "failed to convert integer with 18 decimal to dec"); } #[test] @@ -1158,10 +975,7 @@ mod tests { let expected = FPDecimal::must_from_str("1"); let actual = human_to_dec(integer, decimals); - assert_eq!( - actual, expected, - "failed to convert integer with 18 decimal to dec" - ); + assert_eq!(actual, expected, "failed to convert integer with 18 decimal to dec"); } #[test] @@ -1171,19 +985,13 @@ mod tests { let mut expected = "1000000000000000000000000000000000000"; let actual = human_to_proto(integer, decimals.get_decimals()); - assert_eq!( - actual, expected, - "failed to convert integer with 18 decimal to proto" - ); + assert_eq!(actual, expected, "failed to convert integer with 18 decimal to proto"); decimals = Decimals::Six; expected = "1000000000000000000000000"; let actual = human_to_proto(integer, decimals.get_decimals()); - assert_eq!( - actual, expected, - "failed to convert integer with 6 decimal to proto" - ); + assert_eq!(actual, expected, "failed to convert integer with 6 decimal to proto"); } #[test] @@ -1193,19 +1001,13 @@ mod tests { let mut expected = "1100000000000000000000000000000000000"; let actual = human_to_proto(number, decimals.get_decimals()); - assert_eq!( - actual, expected, - "failed to convert decimal with 18 decimal to proto" - ); + assert_eq!(actual, expected, "failed to convert decimal with 18 decimal to proto"); decimals = Decimals::Six; expected = "1100000000000000000000000"; let actual = human_to_proto(number, decimals.get_decimals()); - assert_eq!( - actual, expected, - "failed to convert decimal with 6 decimal to proto" - ); + assert_eq!(actual, expected, "failed to convert decimal with 6 decimal to proto"); } #[test] @@ -1215,19 +1017,13 @@ mod tests { let mut expected = "100000000000000000000000000000000000"; let actual = human_to_proto(number, decimals.get_decimals()); - assert_eq!( - actual, expected, - "failed to convert decimal with 18 decimal to proto" - ); + assert_eq!(actual, expected, "failed to convert decimal with 18 decimal to proto"); decimals = Decimals::Six; expected = "100000000000000000000000"; let actual = human_to_proto(number, decimals.get_decimals()); - assert_eq!( - actual, expected, - "failed to convert decimal with 6 decimal to proto" - ); + assert_eq!(actual, expected, "failed to convert decimal with 6 decimal to proto"); } #[test] @@ -1237,16 +1033,12 @@ mod tests { let expected = "1000000000000000000"; let actual = human_to_proto(number, decimals.get_decimals()); - assert_eq!( - actual, expected, - "failed to convert decimal with 18 decimal to proto" - ); + assert_eq!(actual, expected, "failed to convert decimal with 18 decimal to proto"); } #[test] #[should_panic] - fn it_panics_when_converting_decimal_below_zero_with_18_decimals_with_too_high_precision_to_proto( - ) { + fn it_panics_when_converting_decimal_below_zero_with_18_decimals_with_too_high_precision_to_proto() { let number = "0.0000000000000000001"; let decimals = Decimals::Eighteen; @@ -1260,10 +1052,7 @@ mod tests { let expected = "1000000000000000000"; let actual = human_to_proto(number, decimals.get_decimals()); - assert_eq!( - actual, expected, - "failed to convert decimal with 6 decimal to proto" - ); + assert_eq!(actual, expected, "failed to convert decimal with 6 decimal to proto"); } #[test] @@ -1272,10 +1061,7 @@ mod tests { let expected = "1000001000000000000"; let actual = human_to_proto(number, 0); - assert_eq!( - actual, expected, - "failed to convert decimal with 0 decimal to proto" - ); + assert_eq!(actual, expected, "failed to convert decimal with 0 decimal to proto"); } #[test] @@ -1284,10 +1070,7 @@ mod tests { let expected = "1000000000000"; let actual = human_to_proto(number, 0); - assert_eq!( - actual, expected, - "failed to convert decimal with 0 decimal to proto" - ); + assert_eq!(actual, expected, "failed to convert decimal with 0 decimal to proto"); } #[test] @@ -1298,8 +1081,7 @@ mod tests { let base_decimals = Decimals::Eighteen; let quote_decimals = Decimals::Six; - let (scaled_price, scaled_quantity) = - scale_price_quantity_for_market(price, quantity, &base_decimals, "e_decimals); + let (scaled_price, scaled_quantity) = scale_price_quantity_for_market(price, quantity, &base_decimals, "e_decimals); // 1 => 1 * 10^6 - 10^18 => 0.000000000001000000 * 10^18 => 1000000 assert_eq!(scaled_price, "1000000", "price was scaled incorrectly"); @@ -1318,8 +1100,7 @@ mod tests { let base_decimals = Decimals::Eighteen; let quote_decimals = Decimals::Six; - let (scaled_price, scaled_quantity) = - scale_price_quantity_for_market(price, quantity, &base_decimals, "e_decimals); + let (scaled_price, scaled_quantity) = scale_price_quantity_for_market(price, quantity, &base_decimals, "e_decimals); // 0.000000000008782000 * 10^18 = 8782000 assert_eq!(scaled_price, "8782000", "price was scaled incorrectly"); @@ -1337,19 +1118,12 @@ mod tests { let base_decimals = Decimals::Six; let quote_decimals = Decimals::Six; - let (scaled_price, scaled_quantity) = - scale_price_quantity_for_market(price, quantity, &base_decimals, "e_decimals); + let (scaled_price, scaled_quantity) = scale_price_quantity_for_market(price, quantity, &base_decimals, "e_decimals); // 1 => 1.(10^18) => 1000000000000000000 - assert_eq!( - scaled_price, "1000000000000000000", - "price was scaled incorrectly" - ); + assert_eq!(scaled_price, "1000000000000000000", "price was scaled incorrectly"); // 1 => 1(10^6).(10^18) => 1000000000000000000000000 - assert_eq!( - scaled_quantity, "1000000000000000000000000", - "quantity was scaled incorrectly" - ); + assert_eq!(scaled_quantity, "1000000000000000000000000", "quantity was scaled incorrectly"); } #[test] @@ -1360,40 +1134,21 @@ mod tests { let base_decimals = Decimals::Six; let quote_decimals = Decimals::Six; - let (scaled_price, scaled_quantity) = - scale_price_quantity_for_market(price, quantity, &base_decimals, "e_decimals); + let (scaled_price, scaled_quantity) = scale_price_quantity_for_market(price, quantity, &base_decimals, "e_decimals); // 1.129 => 1.129(10^15) => 1129000000000000000 - assert_eq!( - scaled_price, "1129000000000000000", - "price was scaled incorrectly" - ); + assert_eq!(scaled_price, "1129000000000000000", "price was scaled incorrectly"); // 1.62 => 1.62(10^4)(10^18) => 1000000000000000000000000 - assert_eq!( - scaled_quantity, "1620000000000000000000000", - "quantity was scaled incorrectly" - ); + assert_eq!(scaled_quantity, "1620000000000000000000000", "quantity was scaled incorrectly"); } } -pub fn are_fpdecimals_approximately_equal( - first: FPDecimal, - second: FPDecimal, - max_diff: FPDecimal, -) -> bool { +pub fn are_fpdecimals_approximately_equal(first: FPDecimal, second: FPDecimal, max_diff: FPDecimal) -> bool { (first - second).abs() <= max_diff } -pub fn assert_fee_is_as_expected( - raw_fees: &mut Vec, - expected_fees: &mut Vec, - max_diff: FPDecimal, -) { - assert_eq!( - raw_fees.len(), - expected_fees.len(), - "Wrong number of fee denoms received" - ); +pub fn assert_fee_is_as_expected(raw_fees: &mut Vec, expected_fees: &mut Vec, max_diff: FPDecimal) { + assert_eq!(raw_fees.len(), expected_fees.len(), "Wrong number of fee denoms received"); raw_fees.sort_by_key(|f| f.denom.clone()); expected_fees.sort_by_key(|f| f.denom.clone()); diff --git a/contracts/swap/src/types.rs b/contracts/swap/src/types.rs index 911f861..34669e9 100644 --- a/contracts/swap/src/types.rs +++ b/contracts/swap/src/types.rs @@ -1,17 +1,17 @@ +use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Coin}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - use injective_cosmwasm::MarketId; use injective_math::FPDecimal; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[cw_serde] pub enum SwapEstimationAmount { InputQuantity(FPCoin), ReceiveQuantity(FPCoin), } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[cw_serde] pub struct FPCoin { pub amount: FPDecimal, pub denom: String, @@ -19,7 +19,7 @@ pub struct FPCoin { impl From for Coin { fn from(value: FPCoin) -> Self { - Coin::new(value.amount.into(), value.denom) + Coin::new(value.amount, value.denom) } } @@ -32,19 +32,19 @@ impl From for FPCoin { } } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[cw_serde] pub struct ConfigResponse { pub config: Config, pub contract_version: String, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[cw_serde] pub enum SwapQuantityMode { MinOutputQuantity(FPDecimal), ExactOutputQuantity(FPDecimal), } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[cw_serde] pub struct StepExecutionEstimate { pub worst_price: FPDecimal, pub result_denom: String, @@ -53,7 +53,7 @@ pub struct StepExecutionEstimate { pub fee_estimate: Option, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[cw_serde] pub struct CurrentSwapOperation { // whole swap operation pub sender_address: Addr, @@ -63,7 +63,7 @@ pub struct CurrentSwapOperation { pub refund: Coin, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[cw_serde] pub struct CurrentSwapStep { // current step pub step_idx: u16, @@ -72,7 +72,7 @@ pub struct CurrentSwapStep { pub is_buy: bool, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[cw_serde] pub struct SwapResults { pub market_id: MarketId, pub quantity: FPDecimal, @@ -80,7 +80,7 @@ pub struct SwapResults { pub fee: FPDecimal, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[cw_serde] pub struct Config { // if fee_recipient is contract, fee discount is replayed to a sender (will not stay in the contract) pub fee_recipient: Addr, @@ -88,7 +88,7 @@ pub struct Config { pub admin: Addr, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[cw_serde] pub struct SwapRoute { pub steps: Vec, pub source_denom: String, @@ -107,13 +107,13 @@ impl SwapRoute { } } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[cw_serde] pub struct SwapStep { pub market_id: MarketId, pub quote_denom: String, // quote for this step of swap, eg for swap eth/inj using eth/usdt and inj/usdt markets, quotes will be eth in 1st step and usdt in 2nd } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[cw_serde] pub struct SwapEstimationResult { pub result_quantity: FPDecimal, pub expected_fees: Vec, diff --git a/docs/admin.md b/docs/admin.md new file mode 100644 index 0000000..6ac7507 --- /dev/null +++ b/docs/admin.md @@ -0,0 +1,71 @@ +# Admin Functionality + +This file contains template transactions for performing the primary functions of managing the Swap contracts. + +## Sentry Node + +Link to [swagger](https://sentry.lcd.injective.network/swagger/). + +### Examples + +```bash +NODE=https://sentry.tm.injective.network:443 +CHAIN_ID=injective-1 +CONTRACT_ADDRESS=inj12yj3mtjarujkhcp6lg3klxjjfrx2v7v8yswgp9 # Helix Swap Contract +``` + +#### Subaccount Open Spot Orders + +```bash +curl -X GET "https://sentry.lcd.injective.network/injective/exchange/v1beta1/spot/orders/$MARKET_ID/$SUBACCOUNT_ID" -H "accept: application/json" | jq . +``` + +#### Subaccount Open Derivative Orders + +```bash +curl -X GET "https://sentry.lcd.injective.network/injective/exchange/v1beta1/derivative/orders/$MARKET_ID/$SUBACCOUNT_ID" -H "accept: application/json" | jq . +``` + +#### Subaccount Deposits + +```bash +curl -X GET "https://sentry.lcd.injective.network/injective/exchange/v1beta1/exchange/subaccountDeposits?subaccount_id=$SUBACCOUNT_ID" -H "accept: application/json" | jq . +``` + +## Wasm + +### Store + +```bash +injectived tx wasm store ./artifacts/grid-aarch64.wasm --from=sgt-account --gas=auto --gas-prices 500000000inj --gas-adjustment 1.3 --yes --output=json --node=https://testnet.sentry.tm.injective.network:443 --chain-id='injective-888' | jq . +``` + +### Instantiate - Derivative + +```bash +injectived tx wasm instantiate $CODE_ID '{"market_type": "derivative", "base_decimals": 18, "quote_decimals": 6, "market_id": "0x17ef48032cb24375ba7c2e39f384e56433bcab20cbee9a7357e4cba2eb00abe6", "small_order_threshold": "10.0"}' --label="inj-usdt-pgt" --admin sgt-account --from=sgt-account --gas=auto --gas-prices 500000000inj --gas-adjustment 1.3 --output=json --node=https://testnet.sentry.tm.injective.network:443 --chain-id='injective-888' +``` + +### Instantiate - Spot + +```bash +injectived tx wasm instantiate $CODE_ID '{"market_type": "spot", "base_decimals": 6, "quote_decimals": 6, "market_id": "0x42edf70cc37e155e9b9f178e04e18999bc8c404bd7b638cc4cbf41da8ef45a21", "valuation_market_id": "0xa508cb32923323679f29a032c70342c147c17d0145625922b0ef22e955c844c0", "small_order_threshold": "10.0"}' --label="qunt-inj-sgt" --admin sgt-account --from=sgt-account --gas=auto --gas-prices 500000000inj --gas-adjustment 1.3 --output=json --node=https://sentry.tm.injective.network:443 --chain-id='injective-1' +``` + +injectived tx wasm instantiate 685 '{"market_type": "spot", "base_decimals": 8, "quote_decimals": 6, "market_id": "0xb03ead807922111939d1b62121ae2956cf6f0a6b03dfdea8d9589c05b98f670f", "small_order_threshold": "10.0"}' --label="w-usdt-sgt" --admin sgt-account --from=sgt-account --gas=auto --gas-prices 500000000inj --gas-adjustment 1.3 --output=json --node=https://sentry.tm.injective.network:443 --chain-id='injective-1' + +### Execute + +#### Create Strategy - Trailing + +```bash +injectived tx wasm execute $CONTRACT_ADDRESS "{'create_strategy': {'subaccount_id': '$SUBACCOUNT_ID', 'bounds': ['1.0', 1.1], 'levels': 10, 'strategy_type': {'trailing_arithmetc': {'lower_trailing_bound': '0.5', 'upper_trailing_bound': '1.5'}}}}" --from=sgt-account --gas=auto --gas-prices 500000000inj --gas-adjustment 1.3 --output=json --yes --node=https://sentry.tm.injective.network:443 --chain-id='injective-1' | jq . +``` + +### Query + +#### Config + +```bash +injectived query wasm contract-state smart $CONTRACT_ADDRESS '{"get_config": {}}' --node=https://sentry.tm.injective.network:443 --output=json | jq . +``` diff --git a/docs/copy_to_devnet_setup.sh b/docs/copy_to_devnet_setup.sh new file mode 100755 index 0000000..2b34097 --- /dev/null +++ b/docs/copy_to_devnet_setup.sh @@ -0,0 +1,13 @@ +#!/bin/bash +ARCH="" + +if [[ $(arch) = "arm64" ]]; then + ARCH=-aarch64 +fi + +COMMIT_HASH=$(git rev-parse --short HEAD) + +rm -f ../devnet-setup/wasm-contracts/swap_contract* +cp artifacts/swap_contract$ARCH.wasm ../devnet-setup/wasm-contracts/swap_contract_${COMMIT_HASH}.wasm + +echo "SWAP_CONVERTER_COMMIT_HASH=$COMMIT_HASH" > "../devnet-setup/wasm-contracts/swap_contract.version" \ No newline at end of file diff --git a/docs/examples.sh b/docs/examples.sh new file mode 100644 index 0000000..6829463 --- /dev/null +++ b/docs/examples.sh @@ -0,0 +1,41 @@ +# Variables +KBT="--keyring-backend test" +CHAIN_ID="injective-1" +HOME="--home ." +GAS_AND_FEE="--gas=6000000 --gas-prices=500000000inj" +NODE="https://k8s.global.mainnet.tm.injective.network:443" +SYNC_MODE="--broadcast-mode sync" +USER="swap-exec" +code_id="67" +ADMIN="inj1exmuhajlxg08l4a59rchsjycxk42dgydg7u62l" + +# MAINNET MARKETS +INJUSDT=0xa508cb32923323679f29a032c70342c147c17d0145625922b0ef22e955c844c0 +ATOMUSDT=0x0511ddc4e6586f3bfe1acb2dd905f8b8a82c97e1edaef654b12ca7e6031ca0fa +WETHUSDT=0xd1956e20d74eeb1febe31cd37060781ff1cb266f49e0512b446a5fafa9a16034 +WMATICUSDT=0xb9a07515a5c239fcbfa3e25eaa829a03d46c4b52b9ab8ee6be471e9eb0e9ea31 +USDCUSDCET=0xda0bb7a7d8361d17a9d2327ed161748f33ecbf02738b45a7dd1d812735d1531c +SOMMUSDT=0x0686357b934c761784d58a2b8b12618dfe557de108a220e06f8f6580abb83aab +GFUSDT=0x7f71c4fba375c964be8db7fc7a5275d974f8c6cdc4d758f2ac4997f106bb052b + +# MAINNET DENOMS +USDT=peggy0xdAC17F958D2ee523a2206206994597C13D831ec7 +WETH=peggy0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 +ATOM=ibc/C4CFF46FD6DE35CA4CF4CE031E643C8FDC9BA4B99AE598E9B0ED98FE3A2319F9 +WMATIC=factory/inj14ejqjyq8um4p3xfqj74yld5waqljf88f9eneuk/inj1dxv423h8ygzgxmxnvrf33ws3k94aedfdevxd8h +SOL=factory/inj14ejqjyq8um4p3xfqj74yld5waqljf88f9eneuk/inj1sthrn5ep8ls5vzz8f9gp89khhmedahhdkqa8z3 +SOMM=ibc/34346A60A95EB030D62D6F5BDD4B745BE18E8A693372A8A347D5D53DBBB1328B +USDCET=factory/inj14ejqjyq8um4p3xfqj74yld5waqljf88f9eneuk/inj1q6zlut7gtkzknkk773jecujwsdkgq882akqksk +GF=peggy0xAaEf88cEa01475125522e117BFe45cF32044E238 + + + +# INSTANTIATE +INIT='{"admin":"'$ADMIN_ADDRESS'", "fee_recipient":{"address": "'$FEE_RECIPIENT_ADDRESS'"}}' +INSTANTIATE_TX_HASH=$(yes 12345678 | injectived tx wasm instantiate $CODE_ID "$INIT" --label="Your Swap Contract" \ +--from=$USER --chain-id="$CHAIN_ID" --yes --admin=$USER $HOME --node=$NODE \ +$GAS_AND_FEE $SYNC_MODE $KBT ) + +# SET ROUTE +INJ_USDT_ROUTE='{"set_route":{"source_denom":"inj","target_denom":"'$USDT'","route":["'$INJUSDT'"]}}' +TX_HASH=$(injectived tx wasm execute $SWAP_CONTRACT_ADDRESS "$INJ_USDT_ROUTE" $HOME --from=$USER $KBT --chain-id=$CHAIN_ID --yes $GAS_AND_FEE --node=$NODE | grep txhash | awk '{print $2}') \ No newline at end of file diff --git a/rust-toolchain.toml b/rust-toolchain.toml index f49bdba..ce4af78 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.73.0" +channel = "1.78.0" components = [ "rustfmt" ] profile = "minimal" targets = [ "wasm32-unknown-unknown" ] From 9b7fcdb06ec6f0357aa0f8ce33bbde3cb9a94230 Mon Sep 17 00:00:00 2001 From: maxrobot Date: Wed, 30 Oct 2024 15:21:04 +0000 Subject: [PATCH 2/9] chore: move helpers --- .cargo/{config => config.toml} | 0 Makefile.toml | 84 ---------------------------------- copy_to_devnet_setup.sh | 13 ------ examples.sh | 41 ----------------- 4 files changed, 138 deletions(-) rename .cargo/{config => config.toml} (100%) delete mode 100644 Makefile.toml delete mode 100755 copy_to_devnet_setup.sh delete mode 100644 examples.sh diff --git a/.cargo/config b/.cargo/config.toml similarity index 100% rename from .cargo/config rename to .cargo/config.toml diff --git a/Makefile.toml b/Makefile.toml deleted file mode 100644 index 9182c98..0000000 --- a/Makefile.toml +++ /dev/null @@ -1,84 +0,0 @@ -[config] -# Set this to `false` to run the tasks at workspace root directory and not on the members -default_to_workspace = false -# Set this to `true` to avoid clashes with core commands (e.g. `cargo make publish` vs `cargo publish`) -skip_core_tasks = true - -[tasks.fmt] -command = "cargo" -args = ["fmt", "--all", "--check"] - -[tasks.test] -command = "cargo" -args = ["test", "--locked"] - -[tasks.lint] -command = "cargo" -args = ["clippy", "--tests", "--", "-D", "warnings"] - -[tasks.build] -command = "cargo" -args = [ - "build", - "--release", - "--locked", - "--target", "wasm32-unknown-unknown", -] - -# This task requires the `cargo-udeps` package: https://crates.io/crates/cargo-udeps -[tasks.udeps] -toolchain = "nightly" -command = "cargo" -args = ["udeps"] - -# This task requires the `cosmwasm-check` package: https://crates.io/crates/cosmwasm-check -[tasks.check] -script = "cosmwasm-check artifacts/*.wasm" - -# This task requires Docker: https://docs.docker.com/get-docker/ -[tasks.optimize] -script = """ -if [[ $(arch) == "arm64" ]]; then - image="cosmwasm/workspace-optimizer-arm64" -else - image="cosmwasm/workspace-optimizer" -fi - -docker run --rm -v "$(pwd)":/code \ - --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ - --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ - ${image}:0.12.9 -""" - -# Update the `contracts` array in the script to reflect the content of your project -[tasks.schema] -script = """ -rm -rf schemas -mkdir schemas - -contracts=( - steak-hub -) - -for contract in ${contracts[@]}; do - cargo run --example schema -p $contract - mv schema/$contract.json schemas -done - -rm -rf schema -""" - -# Update the `crates` array in the script to reflect the content of your project -[tasks.publish] -script = """ -crates=( - steak - steak-hub -) - -for crate in ${crates[@]}; do - cargo publish -p $crate - echo "💤 sleeping for 30 sec before publishing the next crate..." - sleep 30 -done -""" diff --git a/copy_to_devnet_setup.sh b/copy_to_devnet_setup.sh deleted file mode 100755 index 2b34097..0000000 --- a/copy_to_devnet_setup.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -ARCH="" - -if [[ $(arch) = "arm64" ]]; then - ARCH=-aarch64 -fi - -COMMIT_HASH=$(git rev-parse --short HEAD) - -rm -f ../devnet-setup/wasm-contracts/swap_contract* -cp artifacts/swap_contract$ARCH.wasm ../devnet-setup/wasm-contracts/swap_contract_${COMMIT_HASH}.wasm - -echo "SWAP_CONVERTER_COMMIT_HASH=$COMMIT_HASH" > "../devnet-setup/wasm-contracts/swap_contract.version" \ No newline at end of file diff --git a/examples.sh b/examples.sh deleted file mode 100644 index 6829463..0000000 --- a/examples.sh +++ /dev/null @@ -1,41 +0,0 @@ -# Variables -KBT="--keyring-backend test" -CHAIN_ID="injective-1" -HOME="--home ." -GAS_AND_FEE="--gas=6000000 --gas-prices=500000000inj" -NODE="https://k8s.global.mainnet.tm.injective.network:443" -SYNC_MODE="--broadcast-mode sync" -USER="swap-exec" -code_id="67" -ADMIN="inj1exmuhajlxg08l4a59rchsjycxk42dgydg7u62l" - -# MAINNET MARKETS -INJUSDT=0xa508cb32923323679f29a032c70342c147c17d0145625922b0ef22e955c844c0 -ATOMUSDT=0x0511ddc4e6586f3bfe1acb2dd905f8b8a82c97e1edaef654b12ca7e6031ca0fa -WETHUSDT=0xd1956e20d74eeb1febe31cd37060781ff1cb266f49e0512b446a5fafa9a16034 -WMATICUSDT=0xb9a07515a5c239fcbfa3e25eaa829a03d46c4b52b9ab8ee6be471e9eb0e9ea31 -USDCUSDCET=0xda0bb7a7d8361d17a9d2327ed161748f33ecbf02738b45a7dd1d812735d1531c -SOMMUSDT=0x0686357b934c761784d58a2b8b12618dfe557de108a220e06f8f6580abb83aab -GFUSDT=0x7f71c4fba375c964be8db7fc7a5275d974f8c6cdc4d758f2ac4997f106bb052b - -# MAINNET DENOMS -USDT=peggy0xdAC17F958D2ee523a2206206994597C13D831ec7 -WETH=peggy0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 -ATOM=ibc/C4CFF46FD6DE35CA4CF4CE031E643C8FDC9BA4B99AE598E9B0ED98FE3A2319F9 -WMATIC=factory/inj14ejqjyq8um4p3xfqj74yld5waqljf88f9eneuk/inj1dxv423h8ygzgxmxnvrf33ws3k94aedfdevxd8h -SOL=factory/inj14ejqjyq8um4p3xfqj74yld5waqljf88f9eneuk/inj1sthrn5ep8ls5vzz8f9gp89khhmedahhdkqa8z3 -SOMM=ibc/34346A60A95EB030D62D6F5BDD4B745BE18E8A693372A8A347D5D53DBBB1328B -USDCET=factory/inj14ejqjyq8um4p3xfqj74yld5waqljf88f9eneuk/inj1q6zlut7gtkzknkk773jecujwsdkgq882akqksk -GF=peggy0xAaEf88cEa01475125522e117BFe45cF32044E238 - - - -# INSTANTIATE -INIT='{"admin":"'$ADMIN_ADDRESS'", "fee_recipient":{"address": "'$FEE_RECIPIENT_ADDRESS'"}}' -INSTANTIATE_TX_HASH=$(yes 12345678 | injectived tx wasm instantiate $CODE_ID "$INIT" --label="Your Swap Contract" \ ---from=$USER --chain-id="$CHAIN_ID" --yes --admin=$USER $HOME --node=$NODE \ -$GAS_AND_FEE $SYNC_MODE $KBT ) - -# SET ROUTE -INJ_USDT_ROUTE='{"set_route":{"source_denom":"inj","target_denom":"'$USDT'","route":["'$INJUSDT'"]}}' -TX_HASH=$(injectived tx wasm execute $SWAP_CONTRACT_ADDRESS "$INJ_USDT_ROUTE" $HOME --from=$USER $KBT --chain-id=$CHAIN_ID --yes $GAS_AND_FEE --node=$NODE | grep txhash | awk '{print $2}') \ No newline at end of file From a694d8fa983d1734dc26283b202cd661b14f328f Mon Sep 17 00:00:00 2001 From: maxrobot Date: Wed, 30 Oct 2024 21:35:59 +0000 Subject: [PATCH 3/9] 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 { From 255467fb0c029d2206c8b8bc0fc38ef81a466748 Mon Sep 17 00:00:00 2001 From: maxrobot Date: Thu, 31 Oct 2024 20:16:10 +0000 Subject: [PATCH 4/9] feat: migration test --- contracts/swap/src/queries.rs | 205 +-- contracts/swap/src/swap.rs | 17 +- .../src/testing/integration_logic_tests.rs | 1593 ----------------- ...egration_realistic_tests_exact_quantity.rs | 748 ++------ contracts/swap/src/testing/migration_test.rs | 22 +- contracts/swap/src/testing/mod.rs | 3 +- contracts/swap/src/testing/queries_tests.rs | 22 +- contracts/swap/src/testing/test_utils.rs | 223 +-- 8 files changed, 271 insertions(+), 2562 deletions(-) delete mode 100644 contracts/swap/src/testing/integration_logic_tests.rs diff --git a/contracts/swap/src/queries.rs b/contracts/swap/src/queries.rs index dbce82d..1d81edf 100644 --- a/contracts/swap/src/queries.rs +++ b/contracts/swap/src/queries.rs @@ -1,7 +1,5 @@ use cosmwasm_std::{Addr, Deps, Env, StdError, StdResult}; -use injective_cosmwasm::{ - InjectiveQuerier, InjectiveQueryWrapper, MarketId, OrderSide, PriceLevel, SpotMarket, -}; +use injective_cosmwasm::{InjectiveQuerier, InjectiveQueryWrapper, MarketId, OrderSide, PriceLevel, SpotMarket}; use injective_math::utils::round_to_min_tick; use injective_math::FPDecimal; @@ -65,12 +63,8 @@ pub fn estimate_swap_result( env, &step, match swap_quantity { - SwapQuantity::InputQuantity(_) => { - SwapEstimationAmount::InputQuantity(current_swap.clone()) - } - SwapQuantity::OutputQuantity(_) => { - SwapEstimationAmount::ReceiveQuantity(current_swap.clone()) - } + SwapQuantity::InputQuantity(_) => SwapEstimationAmount::InputQuantity(current_swap.clone()), + SwapQuantity::OutputQuantity(_) => SwapEstimationAmount::ReceiveQuantity(current_swap.clone()), }, true, )?; @@ -78,9 +72,10 @@ pub fn estimate_swap_result( current_swap.amount = swap_estimate.result_quantity; current_swap.denom = swap_estimate.result_denom; - let step_fee = swap_estimate - .fee_estimate - .expect("fee estimate should be available"); + deps.api.debug(&format!("step: {:?}", step)); + deps.api.debug(&format!("current_swap: {:?}", current_swap)); + + let step_fee = swap_estimate.fee_estimate.expect("fee estimate should be available"); fees.push(step_fee); } @@ -105,34 +100,21 @@ pub fn estimate_single_swap_execution( SwapEstimationAmount::ReceiveQuantity(fp) => fp, }; - let market = querier - .query_spot_market(market_id)? - .market - .expect("market should be available"); + let market = querier.query_spot_market(market_id)?.market.expect("market should be available"); - let has_invalid_denom = - balance_in.denom != market.quote_denom && balance_in.denom != market.base_denom; + let has_invalid_denom = balance_in.denom != market.quote_denom && balance_in.denom != market.base_denom; if has_invalid_denom { - return Err(StdError::generic_err( - "Invalid swap denom - neither base nor quote", - )); + return Err(StdError::generic_err("Invalid swap denom - neither base nor quote")); } let config = CONFIG.load(deps.storage)?; let is_self_relayer = config.fee_recipient == env.contract.address; - let fee_multiplier = querier - .query_market_atomic_execution_fee_multiplier(market_id)? - .multiplier; + let fee_multiplier = querier.query_market_atomic_execution_fee_multiplier(market_id)?.multiplier; - let fee_percent = market.taker_fee_rate - * fee_multiplier - * (FPDecimal::ONE - get_effective_fee_discount_rate(&market, is_self_relayer)); + let fee_percent = market.taker_fee_rate * fee_multiplier * (FPDecimal::ONE - get_effective_fee_discount_rate(&market, is_self_relayer)); - let is_estimating_from_target = matches!( - swap_estimation_amount, - SwapEstimationAmount::ReceiveQuantity(_) - ); + let is_estimating_from_target = matches!(swap_estimation_amount, SwapEstimationAmount::ReceiveQuantity(_)); let is_buy = if is_estimating_from_target { balance_in.denom == market.base_denom @@ -166,12 +148,7 @@ fn estimate_execution_buy_from_source( ) -> StdResult { let available_swap_quote_funds = input_quote_quantity / (FPDecimal::ONE + fee_percent); - let orders = querier.query_spot_market_orderbook( - &market.market_id, - OrderSide::Sell, - None, - Some(available_swap_quote_funds), - )?; + let orders = querier.query_spot_market_orderbook(&market.market_id, OrderSide::Sell, None, Some(available_swap_quote_funds))?; let top_orders = get_minimum_liquidity_levels( deps, &orders.sells_price_level, @@ -181,8 +158,7 @@ fn estimate_execution_buy_from_source( )?; // lets overestimate amount for buys means rounding average price up -> higher buy price -> worse - let average_price = - get_average_price_from_orders(&top_orders, market.min_price_tick_size, true); + let average_price = get_average_price_from_orders(&top_orders, market.min_price_tick_size, true); let worst_price = get_worst_price_from_orders(&top_orders); let expected_base_quantity = available_swap_quote_funds / average_price; @@ -209,6 +185,9 @@ fn estimate_execution_buy_from_source( ))); } + deps.api.debug(&format!("average_price: {average_price}")); + deps.api.debug(&format!("result_quantity: {result_quantity}")); + Ok(StepExecutionEstimate { worst_price, result_quantity, @@ -230,15 +209,9 @@ fn estimate_execution_buy_from_target( fee_percent: FPDecimal, is_simulation: bool, ) -> StdResult { - let rounded_target_base_output_quantity = - round_up_to_min_tick(target_base_output_quantity, market.min_quantity_tick_size); - - let orders = querier.query_spot_market_orderbook( - &market.market_id, - OrderSide::Sell, - Some(rounded_target_base_output_quantity), - None, - )?; + let rounded_target_base_output_quantity = round_up_to_min_tick(target_base_output_quantity, market.min_quantity_tick_size); + + let orders = querier.query_spot_market_orderbook(&market.market_id, OrderSide::Sell, Some(rounded_target_base_output_quantity), None)?; let top_orders = get_minimum_liquidity_levels( deps, &orders.sells_price_level, @@ -248,8 +221,7 @@ fn estimate_execution_buy_from_target( )?; // lets overestimate amount for buys means rounding average price up -> higher buy price -> worse - let average_price = - get_average_price_from_orders(&top_orders, market.min_price_tick_size, true); + let average_price = get_average_price_from_orders(&top_orders, market.min_price_tick_size, true); let worst_price = get_worst_price_from_orders(&top_orders); let expected_exchange_quote_quantity = rounded_target_base_output_quantity * average_price; @@ -257,8 +229,7 @@ fn estimate_execution_buy_from_target( let required_input_quote_quantity = expected_exchange_quote_quantity + fee_estimate; // check if user funds + contract funds are enough to create order - let required_funds = - worst_price * rounded_target_base_output_quantity * (FPDecimal::ONE + fee_percent); + let required_funds = worst_price * rounded_target_base_output_quantity * (FPDecimal::ONE + fee_percent); let funds_in_contract = deps .querier @@ -304,31 +275,12 @@ fn estimate_execution_buy( SwapEstimationAmount::ReceiveQuantity(fp) => fp, }; - let is_estimating_from_target = matches!( - swap_estimation_amount, - SwapEstimationAmount::ReceiveQuantity(_) - ); + let is_estimating_from_target = matches!(swap_estimation_amount, SwapEstimationAmount::ReceiveQuantity(_)); if is_estimating_from_target { - estimate_execution_buy_from_target( - deps, - querier, - contract_address, - market, - amount_coin.amount, - fee_percent, - is_simulation, - ) + estimate_execution_buy_from_target(deps, querier, contract_address, market, amount_coin.amount, fee_percent, is_simulation) } else { - estimate_execution_buy_from_source( - deps, - querier, - contract_address, - market, - amount_coin.amount, - fee_percent, - is_simulation, - ) + estimate_execution_buy_from_source(deps, querier, contract_address, market, amount_coin.amount, fee_percent, is_simulation) } } @@ -339,12 +291,7 @@ fn estimate_execution_sell_from_source( input_base_quantity: FPDecimal, fee_percent: FPDecimal, ) -> StdResult { - let orders = querier.query_spot_market_orderbook( - &market.market_id, - OrderSide::Buy, - Some(input_base_quantity), - None, - )?; + let orders = querier.query_spot_market_orderbook(&market.market_id, OrderSide::Buy, Some(input_base_quantity), None)?; let top_orders = get_minimum_liquidity_levels( deps, @@ -355,14 +302,18 @@ fn estimate_execution_sell_from_source( )?; // lets overestimate amount for sells means rounding average price down -> lower sell price -> worse - let average_price = - get_average_price_from_orders(&top_orders, market.min_price_tick_size, false); + let average_price = get_average_price_from_orders(&top_orders, market.min_price_tick_size, false); let worst_price = get_worst_price_from_orders(&top_orders); + deps.api.debug(&format!("average_price: {average_price}")); + deps.api.debug(&format!("input_base_quantity: {input_base_quantity}")); + let expected_exchange_quantity = input_base_quantity * average_price; let fee_estimate = expected_exchange_quantity * fee_percent; let expected_quantity = expected_exchange_quantity - fee_estimate; + deps.api.debug(&format!("input_base_quantity: {expected_exchange_quantity}")); + Ok(StepExecutionEstimate { worst_price, result_quantity: expected_quantity, @@ -382,16 +333,10 @@ fn estimate_execution_sell_from_target( target_quote_output_quantity: FPDecimal, fee_percent: FPDecimal, ) -> StdResult { - let required_swap_quantity_in_quote = - target_quote_output_quantity / (FPDecimal::ONE - fee_percent); + let required_swap_quantity_in_quote = target_quote_output_quantity / (FPDecimal::ONE - fee_percent); let required_fee = required_swap_quantity_in_quote - target_quote_output_quantity; - let orders = querier.query_spot_market_orderbook( - &market.market_id, - OrderSide::Buy, - None, - Some(required_swap_quantity_in_quote), - )?; + let orders = querier.query_spot_market_orderbook(&market.market_id, OrderSide::Buy, None, Some(required_swap_quantity_in_quote))?; let top_orders = get_minimum_liquidity_levels( deps, &orders.buys_price_level, @@ -401,18 +346,14 @@ fn estimate_execution_sell_from_target( )?; // lets overestimate amount for sells means rounding average price down -> lower sell price -> worse - let average_price = - get_average_price_from_orders(&top_orders, market.min_price_tick_size, false); + let average_price = get_average_price_from_orders(&top_orders, market.min_price_tick_size, false); let worst_price = get_worst_price_from_orders(&top_orders); let required_swap_input_quantity_in_base = required_swap_quantity_in_quote / average_price; Ok(StepExecutionEstimate { worst_price, - result_quantity: round_up_to_min_tick( - required_swap_input_quantity_in_base, - market.min_quantity_tick_size, - ), + result_quantity: round_up_to_min_tick(required_swap_input_quantity_in_base, market.min_quantity_tick_size), result_denom: market.base_denom.to_string(), is_buy_order: false, fee_estimate: Some(FPCoin { @@ -434,10 +375,7 @@ fn estimate_execution_sell( SwapEstimationAmount::ReceiveQuantity(fp) => fp, }; - let is_estimating_from_target = matches!( - swap_estimation_amount, - SwapEstimationAmount::ReceiveQuantity(_) - ); + let is_estimating_from_target = matches!(swap_estimation_amount, SwapEstimationAmount::ReceiveQuantity(_)); if is_estimating_from_target { estimate_execution_sell_from_target(deps, querier, market, amount_coin.amount, fee_percent) @@ -458,11 +396,7 @@ pub fn get_minimum_liquidity_levels( for level in levels { let value = calc(level); - assert_ne!( - value, - FPDecimal::ZERO, - "Price level with zero value, this should not happen" - ); + assert_ne!(value, FPDecimal::ZERO, "Price level with zero value, this should not happen"); let order_to_add = if sum + value > total { let excess = value + sum - total; @@ -488,24 +422,16 @@ pub fn get_minimum_liquidity_levels( } if sum < total { - return Err(StdError::generic_err( - "Not enough liquidity to fulfill order", - )); + return Err(StdError::generic_err("Not enough liquidity to fulfill order")); } Ok(orders) } -fn get_average_price_from_orders( - levels: &[PriceLevel], - min_price_tick_size: FPDecimal, - is_rounding_up: bool, -) -> FPDecimal { +fn get_average_price_from_orders(levels: &[PriceLevel], min_price_tick_size: FPDecimal, is_rounding_up: bool) -> FPDecimal { let (total_quantity, total_notional) = levels .iter() - .fold((FPDecimal::ZERO, FPDecimal::ZERO), |acc, pl| { - (acc.0 + pl.q, acc.1 + pl.p * pl.q) - }); + .fold((FPDecimal::ZERO, FPDecimal::ZERO), |acc, pl| (acc.0 + pl.q, acc.1 + pl.p * pl.q)); assert_ne!( total_quantity, @@ -543,11 +469,7 @@ mod tests { #[test] fn test_average_price_simple() { - let levels = vec![ - create_price_level(1, 200), - create_price_level(2, 200), - create_price_level(3, 200), - ]; + let levels = vec![create_price_level(1, 200), create_price_level(2, 200), create_price_level(3, 200)]; let avg = get_average_price_from_orders(&levels, FPDecimal::must_from_str("0.01"), false); assert_eq!(avg, FPDecimal::from(2u128)); @@ -555,11 +477,7 @@ mod tests { #[test] fn test_average_price_simple_round_down() { - let levels = vec![ - create_price_level(1, 300), - create_price_level(2, 200), - create_price_level(3, 100), - ]; + let levels = vec![create_price_level(1, 300), create_price_level(2, 200), create_price_level(3, 100)]; let avg = get_average_price_from_orders(&levels, FPDecimal::must_from_str("0.01"), false); assert_eq!(avg, FPDecimal::must_from_str("1.66")); //we round down @@ -567,11 +485,7 @@ mod tests { #[test] fn test_average_price_simple_round_up() { - let levels = vec![ - create_price_level(1, 300), - create_price_level(2, 200), - create_price_level(3, 100), - ]; + let levels = vec![create_price_level(1, 300), create_price_level(2, 200), create_price_level(3, 100)]; let avg = get_average_price_from_orders(&levels, FPDecimal::must_from_str("0.01"), true); assert_eq!(avg, FPDecimal::must_from_str("1.67")); //we round up @@ -579,11 +493,7 @@ mod tests { #[test] fn test_worst_price() { - let levels = vec![ - create_price_level(1, 100), - create_price_level(2, 200), - create_price_level(3, 300), - ]; + let levels = vec![create_price_level(1, 100), create_price_level(2, 200), create_price_level(3, 300)]; let worst = get_worst_price_from_orders(&levels); assert_eq!(worst, FPDecimal::from(3u128)); @@ -601,19 +511,12 @@ mod tests { FPDecimal::must_from_str("0.01"), ); assert!(result.is_err()); - assert_eq!( - result.unwrap_err(), - StdError::generic_err("Not enough liquidity to fulfill order") - ); + assert_eq!(result.unwrap_err(), StdError::generic_err("Not enough liquidity to fulfill order")); } #[test] fn test_find_minimum_orders_with_gaps() { - let levels = vec![ - create_price_level(1, 100), - create_price_level(3, 300), - create_price_level(5, 500), - ]; + let levels = vec![create_price_level(1, 100), create_price_level(3, 300), create_price_level(5, 500)]; let result = get_minimum_liquidity_levels( &inj_mock_deps(|_| {}).as_ref(), @@ -632,11 +535,7 @@ mod tests { #[test] fn test_find_minimum_buy_orders_not_consuming_fully() { - let levels = vec![ - create_price_level(1, 100), - create_price_level(3, 300), - create_price_level(5, 500), - ]; + let levels = vec![create_price_level(1, 100), create_price_level(3, 300), create_price_level(5, 500)]; let result = get_minimum_liquidity_levels( &inj_mock_deps(|_| {}).as_ref(), @@ -658,11 +557,7 @@ mod tests { #[test] fn test_find_minimum_sell_orders_not_consuming_fully() { - let buy_levels = vec![ - create_price_level(5, 500), - create_price_level(3, 300), - create_price_level(1, 100), - ]; + let buy_levels = vec![create_price_level(5, 500), create_price_level(3, 300), create_price_level(1, 100)]; let result = get_minimum_liquidity_levels( &inj_mock_deps(|_| {}).as_ref(), diff --git a/contracts/swap/src/swap.rs b/contracts/swap/src/swap.rs index afff9a4..89b9826 100644 --- a/contracts/swap/src/swap.rs +++ b/contracts/swap/src/swap.rs @@ -158,7 +158,7 @@ pub fn execute_swap_step( pub fn handle_atomic_order_reply(deps: DepsMut, env: Env, msg: Reply) -> Result, ContractError> { let dec_scale_factor = dec_scale_factor(); // protobuf serializes Dec values with extra 10^18 factor - let order_response = MsgCreateSpotMarketOrderResponse::decode(msg.payload.as_slice()).unwrap(); + let order_response = parse_market_order_response(msg)?; let trade_data = match order_response.results { Some(trade_data) => Ok(trade_data), @@ -256,3 +256,18 @@ pub fn handle_atomic_order_reply(deps: DepsMut, env: Env, Ok(response) } + +pub fn parse_market_order_response(msg: Reply) -> StdResult { + let binding = msg.result.into_result().map_err(ContractError::SubMsgFailure).unwrap(); + + let first_messsage = binding.msg_responses.first(); + + let order_response = MsgCreateSpotMarketOrderResponse::decode(first_messsage.unwrap().value.as_slice()) + .map_err(|err| ContractError::ReplyParseFailure { + id: msg.id, + err: err.to_string(), + }) + .unwrap(); + + Ok(order_response) +} diff --git a/contracts/swap/src/testing/integration_logic_tests.rs b/contracts/swap/src/testing/integration_logic_tests.rs deleted file mode 100644 index 2c76100..0000000 --- a/contracts/swap/src/testing/integration_logic_tests.rs +++ /dev/null @@ -1,1593 +0,0 @@ -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 index c3c3f7b..60cf4c9 100644 --- a/contracts/swap/src/testing/integration_realistic_tests_exact_quantity.rs +++ b/contracts/swap/src/testing/integration_realistic_tests_exact_quantity.rs @@ -1,24 +1,19 @@ +use injective_math::FPDecimal; 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, + 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}; @@ -74,9 +69,7 @@ fn it_correctly_swaps_eth_to_get_very_high_exact_amount_of_atom() { 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 _validator = app.get_first_validator_signing_account(INJ.to_string(), 1.2f64).unwrap(); let owner = must_init_account_with_funds( &app, &[ @@ -90,33 +83,21 @@ fn it_correctly_swaps_eth_to_get_very_high_exact_amount_of_atom() { 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)]); 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(), - ], + 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_eth_usdt_buy_orders_from_spreadsheet(&app, &spot_market_1_id, &trader1, &trader2); create_realistic_limit_order( &app, &trader1, @@ -128,13 +109,7 @@ fn it_correctly_swaps_eth_to_get_very_high_exact_amount_of_atom() { 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_atom_usdt_sell_orders_from_spreadsheet(&app, &spot_market_2_id, &trader1, &trader2, &trader3); create_realistic_limit_order( &app, &trader1, @@ -152,18 +127,11 @@ fn it_correctly_swaps_eth_to_get_very_high_exact_amount_of_atom() { let swapper = must_init_account_with_funds( &app, - &[ - str_coin(eth_to_swap, ETH, Decimals::Eighteen), - str_coin("1", INJ, Decimals::Eighteen), - ], + &[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" - ); + 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); @@ -189,15 +157,11 @@ fn it_correctly_swaps_eth_to_get_very_high_exact_amount_of_atom() { ) .unwrap(); - let expected_difference = - human_to_dec(eth_to_swap, Decimals::Eighteen) - query_result.result_quantity; + 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_eq!(swapper_eth_balance_after, expected_difference, "wrong amount of ETH was exchanged"); assert!( swapper_atom_balance_after >= exact_quantity_to_receive, @@ -209,11 +173,7 @@ fn it_correctly_swaps_eth_to_get_very_high_exact_amount_of_atom() { 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, - ), + 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()), @@ -221,31 +181,21 @@ fn it_correctly_swaps_eth_to_get_very_high_exact_amount_of_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" - ); + 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()); + 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 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, - ), + 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()), @@ -287,9 +237,7 @@ fn it_correctly_swaps_inj_to_get_very_high_exact_amount_of_atom() { 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 _validator = app.get_first_validator_signing_account(INJ.to_string(), 1.2f64).unwrap(); let owner = must_init_account_with_funds( &app, &[ @@ -304,33 +252,21 @@ fn it_correctly_swaps_inj_to_get_very_high_exact_amount_of_atom() { 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)], - ); + 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(), - ], + 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_inj_usdt_buy_orders_from_spreadsheet(&app, &spot_market_1_id, &trader1, &trader2); create_realistic_limit_order( &app, &trader1, @@ -342,13 +278,7 @@ fn it_correctly_swaps_inj_to_get_very_high_exact_amount_of_atom() { 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_atom_usdt_sell_orders_from_spreadsheet(&app, &spot_market_2_id, &trader1, &trader2, &trader3); create_realistic_limit_order( &app, &trader1, @@ -366,18 +296,11 @@ fn it_correctly_swaps_inj_to_get_very_high_exact_amount_of_atom() { let swapper = must_init_account_with_funds( &app, - &[ - str_coin(inj_to_swap, INJ_2, Decimals::Eighteen), - str_coin("1", INJ, Decimals::Eighteen), - ], + &[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" - ); + 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"); @@ -404,15 +327,11 @@ fn it_correctly_swaps_inj_to_get_very_high_exact_amount_of_atom() { ) .unwrap(); - let expected_difference = - human_to_dec(inj_to_swap, Decimals::Eighteen) - query_result.result_quantity; + 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_eq!(swapper_inj_balance_after, expected_difference, "wrong amount of INJ was exchanged"); assert!( swapper_atom_balance_after >= exact_quantity_to_receive, @@ -421,15 +340,10 @@ fn it_correctly_swaps_inj_to_get_very_high_exact_amount_of_atom() { 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)); + 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, - ), + 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()), @@ -438,31 +352,21 @@ fn it_correctly_swaps_inj_to_get_very_high_exact_amount_of_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" - ); + 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()); + 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 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, - ), + 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()), @@ -472,18 +376,12 @@ fn it_correctly_swaps_inj_to_get_very_high_exact_amount_of_atom() { #[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"), - ) + 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"), - ) + exact_two_hop_inj_eth_swap_test_template(human_to_dec("0.012", Decimals::Eighteen), Percent("0")) } #[test] @@ -505,9 +403,7 @@ fn it_swaps_inj_to_get_very_high_exact_amount_of_eth() { 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 _validator = app.get_first_validator_signing_account(INJ.to_string(), 1.2f64).unwrap(); let owner = must_init_account_with_funds( &app, &[ @@ -521,33 +417,21 @@ fn it_swaps_inj_to_get_very_high_exact_amount_of_eth() { 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)], - ); + 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(), - ], + 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_inj_usdt_buy_orders_from_spreadsheet(&app, &spot_market_1_id, &trader1, &trader2); create_realistic_limit_order( &app, &trader1, @@ -558,13 +442,7 @@ fn it_swaps_inj_to_get_very_high_exact_amount_of_eth() { 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_eth_usdt_sell_orders_from_spreadsheet(&app, &spot_market_2_id, &trader1, &trader2, &trader3); create_realistic_limit_order( &app, &trader3, @@ -583,18 +461,11 @@ fn it_swaps_inj_to_get_very_high_exact_amount_of_eth() { let swapper = must_init_account_with_funds( &app, - &[ - str_coin(inj_to_swap, INJ_2, Decimals::Eighteen), - str_coin("1", INJ, Decimals::Eighteen), - ], + &[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" - ); + assert_eq!(contract_balances_before.len(), 1, "wrong number of denoms in contract balances"); let query_result: SwapEstimationResult = wasm .query( @@ -618,15 +489,11 @@ fn it_swaps_inj_to_get_very_high_exact_amount_of_eth() { ) .unwrap(); - let expected_difference = - human_to_dec(inj_to_swap, Decimals::Eighteen) - query_result.result_quantity; + 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_eq!(swapper_inj_balance_after, expected_difference, "wrong amount of INJ was exchanged"); assert!( swapper_atom_balance_after >= exact_quantity_to_receive, @@ -636,15 +503,10 @@ fn it_swaps_inj_to_get_very_high_exact_amount_of_eth() { ); 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)); + 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, - ), + 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()), @@ -653,16 +515,10 @@ fn it_swaps_inj_to_get_very_high_exact_amount_of_eth() { ); 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.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()); + 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, @@ -673,11 +529,7 @@ fn it_swaps_inj_to_get_very_high_exact_amount_of_eth() { 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, - ), + 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()), @@ -693,9 +545,7 @@ fn it_correctly_swaps_between_markets_using_different_quote_assets_self_relaying 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 _validator = app.get_first_validator_signing_account(INJ.to_string(), 1.2f64).unwrap(); let owner = must_init_account_with_funds( &app, @@ -713,10 +563,7 @@ fn it_correctly_swaps_between_markets_using_different_quote_assets_self_relaying let contr_addr = init_self_relaying_contract_and_get_address( &wasm, &owner, - &[ - str_coin("10", USDC, Decimals::Six), - str_coin("500", USDT, Decimals::Six), - ], + &[str_coin("10", USDC, Decimals::Six), str_coin("500", USDT, Decimals::Six)], ); set_route_and_assert_success( &wasm, @@ -724,32 +571,18 @@ fn it_correctly_swaps_between_markets_using_different_quote_assets_self_relaying &contr_addr, INJ_2, USDC, - vec![ - spot_market_1_id.as_str().into(), - spot_market_2_id.as_str().into(), - ], + 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_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 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); @@ -788,18 +621,10 @@ fn it_correctly_swaps_between_markets_using_different_quote_assets_self_relaying ]; // 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), - ); + 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" - ); + assert_eq!(contract_balances_before.len(), 2, "wrong number of denoms in contract balances"); wasm.execute( &contr_addr, @@ -815,12 +640,8 @@ fn it_correctly_swaps_between_markets_using_different_quote_assets_self_relaying 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_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); @@ -833,17 +654,11 @@ fn it_correctly_swaps_between_markets_using_different_quote_assets_self_relaying ); 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.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()); + 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, @@ -856,11 +671,7 @@ fn it_correctly_swaps_between_markets_using_different_quote_assets_self_relaying 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, - ), + 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()), @@ -868,10 +679,8 @@ fn it_correctly_swaps_between_markets_using_different_quote_assets_self_relaying ); // 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()); + 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, @@ -884,11 +693,7 @@ fn it_correctly_swaps_between_markets_using_different_quote_assets_self_relaying 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, - ), + 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()), @@ -904,9 +709,7 @@ fn it_correctly_swaps_between_markets_using_different_quote_assets_self_relaying 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 _validator = app.get_first_validator_signing_account(INJ.to_string(), 1.2f64).unwrap(); let owner = must_init_account_with_funds( &app, @@ -937,10 +740,7 @@ fn it_correctly_swaps_between_markets_using_different_quote_assets_self_relaying &contr_addr, USDT, NINJA, - vec![ - spot_market_1_id.as_str().into(), - spot_market_2_id.as_str().into(), - ], + vec![spot_market_1_id.as_str().into(), spot_market_2_id.as_str().into()], ); let trader1 = init_rich_account(&app); @@ -950,13 +750,7 @@ fn it_correctly_swaps_between_markets_using_different_quote_assets_self_relaying 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 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); @@ -990,18 +784,12 @@ fn it_correctly_swaps_between_markets_using_different_quote_assets_self_relaying 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!(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" - ); + assert_eq!(to_balance_after, expected_to_balance_after, "incorrect target amount after swap"); } #[test] @@ -1013,9 +801,7 @@ fn it_doesnt_lose_buffer_if_exact_swap_of_eth_to_atom_is_executed_multiple_times 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 _validator = app.get_first_validator_signing_account(INJ.to_string(), 1.2f64).unwrap(); let owner = must_init_account_with_funds( &app, @@ -1030,11 +816,7 @@ fn it_doesnt_lose_buffer_if_exact_swap_of_eth_to_atom_is_executed_multiple_times 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)]); set_route_and_assert_success( &wasm, @@ -1042,10 +824,7 @@ fn it_doesnt_lose_buffer_if_exact_swap_of_eth_to_atom_is_executed_multiple_times &contr_addr, ETH, ATOM, - vec![ - spot_market_1_id.as_str().into(), - spot_market_2_id.as_str().into(), - ], + vec![spot_market_1_id.as_str().into(), spot_market_2_id.as_str().into()], ); let trader1 = init_rich_account(&app); @@ -1059,9 +838,7 @@ fn it_doesnt_lose_buffer_if_exact_swap_of_eth_to_atom_is_executed_multiple_times &app, &[ str_coin( - (FPDecimal::must_from_str(eth_to_swap) * FPDecimal::from(iterations)) - .to_string() - .as_str(), + (FPDecimal::must_from_str(eth_to_swap) * FPDecimal::from(iterations)).to_string().as_str(), ETH, Decimals::Eighteen, ), @@ -1070,28 +847,13 @@ fn it_doesnt_lose_buffer_if_exact_swap_of_eth_to_atom_is_executed_multiple_times ); 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" - ); + 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, - ); + 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); @@ -1110,16 +872,10 @@ fn it_doesnt_lose_buffer_if_exact_swap_of_eth_to_atom_is_executed_multiple_times } 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.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()); + 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, @@ -1130,14 +886,12 @@ fn it_doesnt_lose_buffer_if_exact_swap_of_eth_to_atom_is_executed_multiple_times // 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()) + 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()) ); } @@ -1150,9 +904,7 @@ fn it_reverts_when_funds_provided_are_below_required_to_get_exact_amount() { 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 _validator = app.get_first_validator_signing_account(INJ.to_string(), 1.2f64).unwrap(); let owner = must_init_account_with_funds( &app, &[ @@ -1167,40 +919,22 @@ fn it_reverts_when_funds_provided_are_below_required_to_get_exact_amount() { 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)], - ); + 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(), - ], + 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, - ); + 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); @@ -1208,18 +942,11 @@ fn it_reverts_when_funds_provided_are_below_required_to_get_exact_amount() { let swapper = must_init_account_with_funds( &app, - &[ - str_coin(inj_to_swap, INJ_2, Decimals::Eighteen), - str_coin("1", INJ, Decimals::Eighteen), - ], + &[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" - ); + 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()); @@ -1247,33 +974,25 @@ fn it_reverts_when_funds_provided_are_below_required_to_get_exact_amount() { ) .unwrap_err(); - assert!(execute_result.to_string().contains("Provided amount of 608000000000000000000 is below required amount of 609714000000000000000"), "wrong error message"); + 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!(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" - ); + 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" - ); + 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()); + 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, @@ -1284,10 +1003,7 @@ fn it_reverts_when_funds_provided_are_below_required_to_get_exact_amount() { // 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, -) { +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); @@ -1295,9 +1011,7 @@ fn exact_two_hop_eth_atom_swap_test_template( 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 _validator = app.get_first_validator_signing_account(INJ.to_string(), 1.2f64).unwrap(); let owner = must_init_account_with_funds( &app, &[ @@ -1311,40 +1025,22 @@ fn exact_two_hop_eth_atom_swap_test_template( 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)]); 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(), - ], + 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, - ); + 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); @@ -1352,18 +1048,11 @@ fn exact_two_hop_eth_atom_swap_test_template( let swapper = must_init_account_with_funds( &app, - &[ - str_coin(eth_to_swap, ETH, Decimals::Eighteen), - str_coin("1", INJ, Decimals::Eighteen), - ], + &[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" - ); + assert_eq!(contract_balances_before.len(), 1, "wrong number of denoms in contract balances"); let query_result: SwapEstimationResult = wasm .query( @@ -1387,18 +1076,13 @@ fn exact_two_hop_eth_atom_swap_test_template( ) .unwrap(); - let expected_difference = - human_to_dec(eth_to_swap, Decimals::Eighteen) - query_result.result_quantity; + 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_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)); + 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, @@ -1408,11 +1092,7 @@ fn exact_two_hop_eth_atom_swap_test_template( ); assert!( - are_fpdecimals_approximately_equal( - swapper_atom_balance_after, - exact_quantity_to_receive, - one_percent_diff, - ), + 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()), @@ -1421,16 +1101,10 @@ fn exact_two_hop_eth_atom_swap_test_template( ); 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.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()); + 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, @@ -1441,11 +1115,7 @@ fn exact_two_hop_eth_atom_swap_test_template( 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, - ), + 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()), @@ -1454,10 +1124,7 @@ fn exact_two_hop_eth_atom_swap_test_template( } // 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, -) { +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); @@ -1465,9 +1132,7 @@ fn exact_two_hop_inj_atom_swap_test_template( 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 _validator = app.get_first_validator_signing_account(INJ.to_string(), 1.2f64).unwrap(); let owner = must_init_account_with_funds( &app, &[ @@ -1482,40 +1147,22 @@ fn exact_two_hop_inj_atom_swap_test_template( 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)], - ); + 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(), - ], + 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, - ); + 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); @@ -1523,18 +1170,11 @@ fn exact_two_hop_inj_atom_swap_test_template( let swapper = must_init_account_with_funds( &app, - &[ - str_coin(inj_to_swap, INJ_2, Decimals::Eighteen), - str_coin("1", INJ, Decimals::Eighteen), - ], + &[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" - ); + assert_eq!(contract_balances_before.len(), 1, "wrong number of denoms in contract balances"); let query_result: SwapEstimationResult = wasm .query( @@ -1558,15 +1198,11 @@ fn exact_two_hop_inj_atom_swap_test_template( ) .unwrap(); - let expected_difference = - human_to_dec(inj_to_swap, Decimals::Eighteen) - query_result.result_quantity; + 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_eq!(swapper_inj_balance_after, expected_difference, "wrong amount of INJ was exchanged"); assert!( swapper_atom_balance_after >= exact_quantity_to_receive, @@ -1575,15 +1211,10 @@ fn exact_two_hop_inj_atom_swap_test_template( 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)); + 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, - ), + 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()), @@ -1592,16 +1223,10 @@ fn exact_two_hop_inj_atom_swap_test_template( ); 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.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()); + 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, @@ -1612,11 +1237,7 @@ fn exact_two_hop_inj_atom_swap_test_template( 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, - ), + 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()), @@ -1625,10 +1246,7 @@ fn exact_two_hop_inj_atom_swap_test_template( } // source much cheaper than target -fn exact_two_hop_inj_eth_swap_test_template( - exact_quantity_to_receive: FPDecimal, - max_diff_percentage: Percent, -) { +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); @@ -1636,9 +1254,7 @@ fn exact_two_hop_inj_eth_swap_test_template( 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 _validator = app.get_first_validator_signing_account(INJ.to_string(), 1.2f64).unwrap(); let owner = must_init_account_with_funds( &app, &[ @@ -1652,40 +1268,22 @@ fn exact_two_hop_inj_eth_swap_test_template( 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)], - ); + 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(), - ], + 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, - ); + 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); @@ -1693,18 +1291,11 @@ fn exact_two_hop_inj_eth_swap_test_template( let swapper = must_init_account_with_funds( &app, - &[ - str_coin(inj_to_swap, INJ_2, Decimals::Eighteen), - str_coin("1", INJ, Decimals::Eighteen), - ], + &[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" - ); + assert_eq!(contract_balances_before.len(), 1, "wrong number of denoms in contract balances"); let query_result: SwapEstimationResult = wasm .query( @@ -1728,15 +1319,11 @@ fn exact_two_hop_inj_eth_swap_test_template( ) .unwrap(); - let expected_difference = - human_to_dec(inj_to_swap, Decimals::Eighteen) - query_result.result_quantity; + 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_eq!(swapper_inj_balance_after, expected_difference, "wrong amount of INJ was exchanged"); assert!( swapper_atom_balance_after >= exact_quantity_to_receive, @@ -1745,15 +1332,10 @@ fn exact_two_hop_inj_eth_swap_test_template( 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)); + 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, - ), + 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()), @@ -1762,16 +1344,10 @@ fn exact_two_hop_inj_eth_swap_test_template( ); 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.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()); + 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, @@ -1782,11 +1358,7 @@ fn exact_two_hop_inj_eth_swap_test_template( 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, - ), + 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()), diff --git a/contracts/swap/src/testing/migration_test.rs b/contracts/swap/src/testing/migration_test.rs index 73dc413..746e906 100644 --- a/contracts/swap/src/testing/migration_test.rs +++ b/contracts/swap/src/testing/migration_test.rs @@ -2,30 +2,24 @@ use crate::{ msg::{FeeRecipient, InstantiateMsg, MigrateMsg}, testing::{ integration_realistic_tests_min_quantity::happy_path_two_hops_test, - test_utils::{ - must_init_account_with_funds, store_code, str_coin, Decimals, ATOM, ETH, INJ, USDT, - }, + test_utils::{must_init_account_with_funds, str_coin, Decimals, ATOM, ETH, INJ, USDT}, }, }; -use cosmos_sdk_proto::cosmwasm::wasm::v1::{ - MsgMigrateContract, MsgMigrateContractResponse, QueryContractInfoRequest, - QueryContractInfoResponse, -}; use cosmwasm_std::Addr; +use injective_std::types::cosmwasm::wasm::v1::{MsgMigrateContract, MsgMigrateContractResponse, QueryContractInfoRequest, QueryContractInfoResponse}; use injective_test_tube::{Account, ExecuteResponse, InjectiveTestApp, Module, Runner, Wasm}; +use injective_testing::test_tube::utils::store_code; type V100InstantiateMsg = InstantiateMsg; #[test] #[cfg_attr(not(feature = "integration"), ignore)] -fn test_migration_v100_to_v101() { +fn test_migration() { let app = InjectiveTestApp::new(); let wasm = Wasm::new(&app); - let wasm_byte_code = - std::fs::read("../../contracts/swap/src/testing/test_artifacts/swap-contract-v100.wasm") - .unwrap(); + let wasm_byte_code = std::fs::read("../../contracts/swap/src/testing/test_artifacts/swap-contract-v101.wasm").unwrap(); let owner = must_init_account_with_funds( &app, @@ -37,11 +31,7 @@ fn test_migration_v100_to_v101() { ], ); - let swap_v100_code_id = wasm - .store_code(&wasm_byte_code, None, &owner) - .unwrap() - .data - .code_id; + let swap_v100_code_id = wasm.store_code(&wasm_byte_code, None, &owner).unwrap().data.code_id; let swap_v100_address: String = wasm .instantiate( diff --git a/contracts/swap/src/testing/mod.rs b/contracts/swap/src/testing/mod.rs index 0b0eead..ac28e14 100644 --- a/contracts/swap/src/testing/mod.rs +++ b/contracts/swap/src/testing/mod.rs @@ -1,9 +1,8 @@ mod authz_tests; mod config_tests; -// // mod migration_test; -mod integration_logic_tests; mod integration_realistic_tests_exact_quantity; mod integration_realistic_tests_min_quantity; +mod migration_test; mod queries_tests; mod storage_tests; mod swap_tests; diff --git a/contracts/swap/src/testing/queries_tests.rs b/contracts/swap/src/testing/queries_tests.rs index 5e714f5..e79372e 100644 --- a/contracts/swap/src/testing/queries_tests.rs +++ b/contracts/swap/src/testing/queries_tests.rs @@ -1,7 +1,7 @@ use std::ops::Neg; use std::str::FromStr; -use cosmwasm_std::testing::{mock_env, mock_info}; +use cosmwasm_std::testing::{message_info, mock_env}; use cosmwasm_std::{coin, Addr}; use crate::admin::set_route; @@ -32,7 +32,7 @@ fn test_calculate_swap_price_external_fee_recipient_from_source_quantity() { instantiate( deps.as_mut_deps(), mock_env(), - mock_info(admin.as_ref(), &[coin(1_000u128, "usdt")]), + message_info(&Addr::unchecked(admin), &[coin(1_000u128, "usdt")]), InstantiateMsg { fee_recipient: FeeRecipient::Address(admin.to_owned()), admin: admin.to_owned(), @@ -107,7 +107,7 @@ fn test_calculate_swap_price_external_fee_recipient_from_target_quantity() { instantiate( deps.as_mut_deps(), mock_env(), - mock_info(admin.as_ref(), &[coin(1_000u128, "usdt")]), + message_info(&Addr::unchecked(admin), &[coin(1_000u128, "usdt")]), InstantiateMsg { fee_recipient: FeeRecipient::Address(admin.to_owned()), admin: admin.to_owned(), @@ -181,7 +181,7 @@ fn test_calculate_swap_price_self_fee_recipient_from_source_quantity() { instantiate( deps.as_mut_deps(), mock_env(), - mock_info(admin.as_ref(), &[coin(1_000u128, "usdt")]), + message_info(&Addr::unchecked(admin), &[coin(1_000u128, "usdt")]), InstantiateMsg { fee_recipient: FeeRecipient::SwapContract, admin: admin.to_owned(), @@ -252,7 +252,7 @@ fn test_calculate_swap_price_self_fee_recipient_from_target_quantity() { instantiate( deps.as_mut_deps(), mock_env(), - mock_info(admin.as_ref(), &[coin(1_000u128, "usdt")]), + message_info(&Addr::unchecked(admin), &[coin(1_000u128, "usdt")]), InstantiateMsg { fee_recipient: FeeRecipient::SwapContract, admin: admin.to_owned(), @@ -329,7 +329,7 @@ fn test_calculate_estimate_when_selling_both_quantity_directions_simple() { instantiate( deps.as_mut_deps(), mock_env(), - mock_info(admin.as_ref(), &[coin(1_000u128, "usdt")]), + message_info(&Addr::unchecked(admin), &[coin(1_000u128, "usdt")]), InstantiateMsg { fee_recipient: FeeRecipient::Address(admin.to_owned()), admin: admin.to_owned(), @@ -433,7 +433,7 @@ fn test_calculate_estimate_when_buying_both_quantity_directions_simple() { instantiate( deps.as_mut_deps(), mock_env(), - mock_info(admin.as_ref(), &[coin(1_000u128, "usdt")]), + message_info(&Addr::unchecked(admin), &[coin(1_000u128, "usdt")]), InstantiateMsg { fee_recipient: FeeRecipient::Address(admin.to_owned()), admin: admin.to_owned(), @@ -522,7 +522,7 @@ fn get_all_queries_returns_empty_array_if_no_routes_are_set() { instantiate( deps.as_mut_deps(), mock_env(), - mock_info(admin.as_ref(), &[coin(1_000u128, "usdt")]), + message_info(&Addr::unchecked(admin), &[coin(1_000u128, "usdt")]), InstantiateMsg { fee_recipient: FeeRecipient::SwapContract, admin: admin.to_owned(), @@ -544,7 +544,7 @@ fn get_all_queries_returns_expected_array_if_routes_are_set() { instantiate( deps.as_mut_deps(), mock_env(), - mock_info(admin.as_ref(), &[coin(1_000u128, "usdt")]), + message_info(&Addr::unchecked(admin), &[coin(1_000u128, "usdt")]), InstantiateMsg { fee_recipient: FeeRecipient::SwapContract, admin: admin.to_owned(), @@ -607,6 +607,6 @@ fn get_all_queries_returns_expected_array_if_routes_are_set() { "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); + let all_routes_result_paginated = get_all_swap_routes(deps.as_ref().storage, None, Some(1u32)); + assert_eq!(all_routes_result_paginated.unwrap().len(), 1); } diff --git a/contracts/swap/src/testing/test_utils.rs b/contracts/swap/src/testing/test_utils.rs index b0c2c5a..85c8ca5 100644 --- a/contracts/swap/src/testing/test_utils.rs +++ b/contracts/swap/src/testing/test_utils.rs @@ -13,18 +13,17 @@ use injective_std::{ types::{ cosmos::{ authz::v1beta1::{Grant, MsgGrant}, - bank::v1beta1::{MsgSend, QueryAllBalancesRequest, QueryBalanceRequest}, - base::v1beta1::Coin as TubeCoin, - gov::{v1::MsgVote, v1beta1::MsgSubmitProposal}, + bank::v1beta1::{QueryAllBalancesRequest, QueryBalanceRequest}, }, cosmwasm::wasm::v1::{AcceptedMessageKeysFilter, ContractExecutionAuthorization, ContractGrant, MaxCallsLimit}, - injective::exchange::v1beta1::{ - MsgCreateSpotLimitOrder, OrderInfo, OrderType, QuerySpotMarketsRequest, SpotMarketParamUpdateProposal, SpotOrder, - }, + injective::exchange::v1beta1::{MsgCreateSpotLimitOrder, OrderInfo, OrderType, 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 injective_test_tube::{Account, Authz, Bank, Exchange, InjectiveTestApp, Module, SigningAccount, Wasm}; +use injective_testing::{ + test_tube::{exchange::launch_spot_market_custom, utils::store_code}, + utils::scale_price_quantity_spot_market, +}; use prost::Message; use std::{collections::HashMap, str::FromStr}; @@ -47,7 +46,6 @@ pub const NINJA: &str = "ninja"; pub const DEFAULT_TAKER_FEE: f64 = 0.001; pub const DEFAULT_ATOMIC_MULTIPLIER: f64 = 2.5; pub const DEFAULT_SELF_RELAYING_FEE_PART: f64 = 0.6; -pub const DEFAULT_RELAYER_SHARE: f64 = 1.0 - DEFAULT_SELF_RELAYING_FEE_PART; #[derive(PartialEq, Eq, Debug, Copy, Clone)] #[repr(i32)] @@ -276,29 +274,16 @@ fn create_mock_spot_market(base: &str, min_price_tick_size: FPDecimal, min_quant min_quantity_tick_size, } } -pub fn get_spot_market_id(exchange: &Exchange, ticker: String) -> String { - let spot_markets = exchange - .query_spot_markets(&QuerySpotMarketsRequest { - status: "Active".to_string(), - market_ids: vec![], - }) - .unwrap() - .markets; - - let market = spot_markets.iter().find(|m| m.ticker == ticker).unwrap(); - - market.market_id.to_string() -} pub fn launch_realistic_inj_usdt_spot_market(exchange: &Exchange, signer: &SigningAccount) -> String { launch_spot_market_custom( exchange, signer, - "TICKER".to_string(), + "INJ2/USDT".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")), + "0.000000000000001".to_string(), + "1000000000000000".to_string(), ) } @@ -309,8 +294,8 @@ pub fn launch_realistic_weth_usdt_spot_market(exchange: &Exchange (String, String) { - let price_dec = FPDecimal::must_from_str(price.replace('_', "").as_str()); - let quantity_dec = FPDecimal::must_from_str(quantity.replace('_', "").as_str()); - - let scaled_price = price_dec.scaled(quote_decimals.get_decimals() - base_decimals.get_decimals()); - let scaled_quantity = quantity_dec.scaled(base_decimals.get_decimals()); - (dec_to_proto(scaled_price), dec_to_proto(scaled_quantity)) -} - -pub fn dec_to_proto(val: FPDecimal) -> String { - val.scaled(18).to_string() -} - #[allow(clippy::too_many_arguments)] pub fn create_realistic_limit_order( app: &InjectiveTestApp, @@ -598,7 +542,7 @@ pub fn create_realistic_limit_order( base_decimals: Decimals, quote_decimals: Decimals, ) { - let (price_to_send, quantity_to_send) = scale_price_quantity_for_market(price, quantity, &base_decimals, "e_decimals); + let (price_to_send, quantity_to_send) = scale_price_quantity_spot_market(price, quantity, &(base_decimals as i32), &(quote_decimals as i32)); let exchange = Exchange::new(app); exchange @@ -645,29 +589,6 @@ pub fn init_self_relaying_contract_and_get_address(wasm: &Wasm .address } -pub fn init_contract_with_fee_recipient_and_get_address( - wasm: &Wasm, - owner: &SigningAccount, - initial_balance: &[Coin], - fee_recipient: &SigningAccount, -) -> String { - let code_id = store_code(wasm, owner, "swap_contract".to_string()); - wasm.instantiate( - code_id, - &InstantiateMsg { - fee_recipient: FeeRecipient::Address(Addr::unchecked(fee_recipient.address())), - admin: Addr::unchecked(owner.address()), - }, - Some(&owner.address()), - Some("Swap"), - initial_balance, - owner, - ) - .unwrap() - .data - .address -} - pub fn set_route_and_assert_success( wasm: &Wasm, signer: &SigningAccount, @@ -718,31 +639,6 @@ pub fn query_bank_balance(bank: &Bank, denom: &str, address: & .unwrap() } -pub fn pause_spot_market(app: &InjectiveTestApp, market_id: &str, proposer: &SigningAccount, validator: &SigningAccount) { - let gov = Gov::new(app); - pass_spot_market_params_update_proposal( - &gov, - &SpotMarketParamUpdateProposal { - title: format!("Set market {market_id} status to paused"), - description: format!("Set market {market_id} status to paused"), - market_id: market_id.to_string(), - maker_fee_rate: "".to_string(), - taker_fee_rate: "".to_string(), - relayer_fee_share_rate: "".to_string(), - min_price_tick_size: "".to_string(), - min_quantity_tick_size: "".to_string(), - status: 2, - min_notional: "0".to_string(), - ticker: "0".to_string(), - admin_info: None, - }, - proposer, - validator, - ); - - app.increase_time(10u64) -} - pub fn create_contract_authorization( app: &InjectiveTestApp, contract: String, @@ -799,56 +695,6 @@ pub fn create_contract_authorization( .unwrap(); } -pub fn pass_spot_market_params_update_proposal( - gov: &Gov, - proposal: &SpotMarketParamUpdateProposal, - proposer: &SigningAccount, - validator: &SigningAccount, -) { - let mut buf = vec![]; - SpotMarketParamUpdateProposal::encode(proposal, &mut buf).unwrap(); - - println!("submitting proposal: {proposal:?}"); - let submit_response = gov.submit_proposal_v1beta1( - MsgSubmitProposal { - content: Some(Any { - type_url: "/injective.exchange.v1beta1.SpotMarketParamUpdateProposal".to_string(), - value: buf, - }), - initial_deposit: vec![TubeCoin { - amount: "100000000000000000000".to_string(), - denom: "inj".to_string(), - }], - proposer: proposer.address(), - }, - proposer, - ); - - assert!(submit_response.is_ok(), "failed to submit proposal"); - - let proposal_id = submit_response.unwrap().data.proposal_id; - println!("voting on proposal: {proposal_id:?}"); - let vote_response = gov.vote( - MsgVote { - proposal_id, - voter: validator.address(), - option: 1, - metadata: "".to_string(), - }, - validator, - ); - - assert!(vote_response.is_ok(), "failed to vote on proposal"); -} - -pub fn init_default_validator_account(app: &InjectiveTestApp) -> SigningAccount { - app.get_first_validator_signing_account(INJ.to_string(), 1.2f64).unwrap() -} - -pub fn init_default_signer_account(app: &InjectiveTestApp) -> SigningAccount { - must_init_account_with_funds(app, &[str_coin("100_000", INJ, Decimals::Eighteen)]) -} - pub fn init_rich_account(app: &InjectiveTestApp) -> SigningAccount { must_init_account_with_funds( app, @@ -864,21 +710,6 @@ pub fn init_rich_account(app: &InjectiveTestApp) -> SigningAccount { ) } -pub fn fund_account_with_some_inj(bank: &Bank, from: &SigningAccount, to: &SigningAccount) { - bank.send( - MsgSend { - from_address: from.address(), - to_address: to.address(), - amount: vec![TubeCoin { - amount: "1000000000000000000000".to_string(), - denom: "inj".to_string(), - }], - }, - from, - ) - .unwrap(); -} - pub fn human_to_dec(raw_number: &str, decimals: Decimals) -> FPDecimal { FPDecimal::must_from_str(&raw_number.replace('_', "")).scaled(decimals.get_decimals()) } @@ -894,7 +725,7 @@ pub fn str_coin(human_amount: &str, denom: &str, decimals: Decimals) -> Coin { } mod tests { - use crate::testing::test_utils::{human_to_dec, human_to_proto, scale_price_quantity_for_market, Decimals}; + use crate::testing::test_utils::{human_to_dec, human_to_proto, scale_price_quantity_spot_market, Decimals}; use injective_math::FPDecimal; #[test] @@ -1088,7 +919,7 @@ mod tests { let base_decimals = Decimals::Eighteen; let quote_decimals = Decimals::Six; - let (scaled_price, scaled_quantity) = scale_price_quantity_for_market(price, quantity, &base_decimals, "e_decimals); + let (scaled_price, scaled_quantity) = scale_price_quantity_spot_market(price, quantity, &(base_decimals as i32), &(quote_decimals as i32)); // 1 => 1 * 10^6 - 10^18 => 0.000000000001000000 * 10^18 => 1000000 assert_eq!(scaled_price, "1000000", "price was scaled incorrectly"); @@ -1107,7 +938,7 @@ mod tests { let base_decimals = Decimals::Eighteen; let quote_decimals = Decimals::Six; - let (scaled_price, scaled_quantity) = scale_price_quantity_for_market(price, quantity, &base_decimals, "e_decimals); + let (scaled_price, scaled_quantity) = scale_price_quantity_spot_market(price, quantity, &(base_decimals as i32), &(quote_decimals as i32)); // 0.000000000008782000 * 10^18 = 8782000 assert_eq!(scaled_price, "8782000", "price was scaled incorrectly"); @@ -1125,7 +956,7 @@ mod tests { let base_decimals = Decimals::Six; let quote_decimals = Decimals::Six; - let (scaled_price, scaled_quantity) = scale_price_quantity_for_market(price, quantity, &base_decimals, "e_decimals); + let (scaled_price, scaled_quantity) = scale_price_quantity_spot_market(price, quantity, &(base_decimals as i32), &(quote_decimals as i32)); // 1 => 1.(10^18) => 1000000000000000000 assert_eq!(scaled_price, "1000000000000000000", "price was scaled incorrectly"); @@ -1141,7 +972,7 @@ mod tests { let base_decimals = Decimals::Six; let quote_decimals = Decimals::Six; - let (scaled_price, scaled_quantity) = scale_price_quantity_for_market(price, quantity, &base_decimals, "e_decimals); + let (scaled_price, scaled_quantity) = scale_price_quantity_spot_market(price, quantity, &(base_decimals as i32), &(quote_decimals as i32)); // 1.129 => 1.129(10^15) => 1129000000000000000 assert_eq!(scaled_price, "1129000000000000000", "price was scaled incorrectly"); @@ -1154,7 +985,7 @@ pub fn are_fpdecimals_approximately_equal(first: FPDecimal, second: FPDecimal, m (first - second).abs() <= max_diff } -pub fn assert_fee_is_as_expected(raw_fees: &mut Vec, expected_fees: &mut Vec, max_diff: FPDecimal) { +pub fn assert_fee_is_as_expected(raw_fees: &mut [FPCoin], expected_fees: &mut [FPCoin], max_diff: FPDecimal) { assert_eq!(raw_fees.len(), expected_fees.len(), "Wrong number of fee denoms received"); raw_fees.sort_by_key(|f| f.denom.clone()); From 37ad16a0027bb8e0e4fbe199cb546be3c40e4835 Mon Sep 17 00:00:00 2001 From: maxrobot Date: Thu, 31 Oct 2024 20:22:27 +0000 Subject: [PATCH 5/9] chore: finish migration --- contracts/swap/src/queries.rs | 11 -------- contracts/swap/src/testing/migration_test.rs | 24 +++++++++--------- .../test_artifacts/swap-contract-v100.wasm | Bin 549994 -> 0 bytes .../test_artifacts/swap_contract-v101.wasm | Bin 0 -> 564840 bytes 4 files changed, 12 insertions(+), 23 deletions(-) delete mode 100644 contracts/swap/src/testing/test_artifacts/swap-contract-v100.wasm create mode 100644 contracts/swap/src/testing/test_artifacts/swap_contract-v101.wasm diff --git a/contracts/swap/src/queries.rs b/contracts/swap/src/queries.rs index 1d81edf..fa1ce87 100644 --- a/contracts/swap/src/queries.rs +++ b/contracts/swap/src/queries.rs @@ -72,9 +72,6 @@ pub fn estimate_swap_result( current_swap.amount = swap_estimate.result_quantity; current_swap.denom = swap_estimate.result_denom; - deps.api.debug(&format!("step: {:?}", step)); - deps.api.debug(&format!("current_swap: {:?}", current_swap)); - let step_fee = swap_estimate.fee_estimate.expect("fee estimate should be available"); fees.push(step_fee); @@ -185,9 +182,6 @@ fn estimate_execution_buy_from_source( ))); } - deps.api.debug(&format!("average_price: {average_price}")); - deps.api.debug(&format!("result_quantity: {result_quantity}")); - Ok(StepExecutionEstimate { worst_price, result_quantity, @@ -305,15 +299,10 @@ fn estimate_execution_sell_from_source( let average_price = get_average_price_from_orders(&top_orders, market.min_price_tick_size, false); let worst_price = get_worst_price_from_orders(&top_orders); - deps.api.debug(&format!("average_price: {average_price}")); - deps.api.debug(&format!("input_base_quantity: {input_base_quantity}")); - let expected_exchange_quantity = input_base_quantity * average_price; let fee_estimate = expected_exchange_quantity * fee_percent; let expected_quantity = expected_exchange_quantity - fee_estimate; - deps.api.debug(&format!("input_base_quantity: {expected_exchange_quantity}")); - Ok(StepExecutionEstimate { worst_price, result_quantity: expected_quantity, diff --git a/contracts/swap/src/testing/migration_test.rs b/contracts/swap/src/testing/migration_test.rs index 746e906..bf9370d 100644 --- a/contracts/swap/src/testing/migration_test.rs +++ b/contracts/swap/src/testing/migration_test.rs @@ -11,7 +11,7 @@ use injective_std::types::cosmwasm::wasm::v1::{MsgMigrateContract, MsgMigrateCon use injective_test_tube::{Account, ExecuteResponse, InjectiveTestApp, Module, Runner, Wasm}; use injective_testing::test_tube::utils::store_code; -type V100InstantiateMsg = InstantiateMsg; +type V101InstantiateMsg = InstantiateMsg; #[test] #[cfg_attr(not(feature = "integration"), ignore)] @@ -31,12 +31,12 @@ fn test_migration() { ], ); - let swap_v100_code_id = wasm.store_code(&wasm_byte_code, None, &owner).unwrap().data.code_id; + let swap_v101_code_id = wasm.store_code(&wasm_byte_code, None, &owner).unwrap().data.code_id; - let swap_v100_address: String = wasm + let swap_v101_address: String = wasm .instantiate( - swap_v100_code_id, - &V100InstantiateMsg { + swap_v101_code_id, + &V101InstantiateMsg { admin: Addr::unchecked(owner.address()), fee_recipient: FeeRecipient::SwapContract, }, @@ -53,14 +53,14 @@ fn test_migration() { .query( "/cosmwasm.wasm.v1.Query/ContractInfo", &QueryContractInfoRequest { - address: swap_v100_address.clone(), + address: swap_v101_address.clone(), }, ) .unwrap(); let contract_info = res.contract_info.unwrap(); - assert_eq!(res.address, swap_v100_address); - assert_eq!(contract_info.code_id, swap_v100_code_id); + assert_eq!(res.address, swap_v101_address); + assert_eq!(contract_info.code_id, swap_v101_code_id); assert_eq!(contract_info.creator, owner.address()); assert_eq!(contract_info.label, "swap-contract"); @@ -70,7 +70,7 @@ fn test_migration() { .execute( MsgMigrateContract { sender: owner.address(), - contract: swap_v100_address.clone(), + contract: swap_v101_address.clone(), code_id: swap_v110_code_id, msg: serde_json_wasm::to_vec(&MigrateMsg {}).unwrap(), }, @@ -83,17 +83,17 @@ fn test_migration() { .query( "/cosmwasm.wasm.v1.Query/ContractInfo", &QueryContractInfoRequest { - address: swap_v100_address.clone(), + address: swap_v101_address.clone(), }, ) .unwrap(); let contract_info = res.contract_info.unwrap(); - assert_eq!(res.address, swap_v100_address); + assert_eq!(res.address, swap_v101_address); assert_eq!(contract_info.code_id, swap_v110_code_id); assert_eq!(contract_info.creator, owner.address()); assert_eq!(contract_info.label, "swap-contract"); - happy_path_two_hops_test(app, owner, swap_v100_address); + happy_path_two_hops_test(app, owner, swap_v101_address); } diff --git a/contracts/swap/src/testing/test_artifacts/swap-contract-v100.wasm b/contracts/swap/src/testing/test_artifacts/swap-contract-v100.wasm deleted file mode 100644 index 4bdd560712328985009c10daa7f9da0f9c609fc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 549994 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%0Dfxrn*-Gd5Z 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 A(f|Me diff --git a/contracts/swap/src/testing/test_artifacts/swap_contract-v101.wasm b/contracts/swap/src/testing/test_artifacts/swap_contract-v101.wasm new file mode 100644 index 0000000000000000000000000000000000000000..59d7e8aff55b845eba8370e440c229a27936a20a GIT binary patch literal 564840 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�bmONv2(TpbwC!7W<~spv@g=b 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 literal 0 HcmV?d00001 From 4db805c2be8af0cf8cb3b0798fcdad16216b34c0 Mon Sep 17 00:00:00 2001 From: maxrobot Date: Thu, 31 Oct 2024 20:24:43 +0000 Subject: [PATCH 6/9] chore: ci --- .github/workflows/rust.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml index 8d93b9b..5ee8582 100644 --- a/.github/workflows/rust.yaml +++ b/.github/workflows/rust.yaml @@ -33,7 +33,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: 1.74.0 + toolchain: 1.78.0 target: wasm32-unknown-unknown override: true components: llvm-tools-preview @@ -98,7 +98,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: 1.74.0 + toolchain: 1.78.0 override: true components: rustfmt, clippy From 23201dba68d0c65071330c913bb74abac90d3b65 Mon Sep 17 00:00:00 2001 From: maxrobot Date: Thu, 31 Oct 2024 20:26:16 +0000 Subject: [PATCH 7/9] admin doc --- docs/admin.md | 36 +----------------------------------- 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/docs/admin.md b/docs/admin.md index 6ac7507..f8fad7b 100644 --- a/docs/admin.md +++ b/docs/admin.md @@ -20,46 +20,12 @@ CONTRACT_ADDRESS=inj12yj3mtjarujkhcp6lg3klxjjfrx2v7v8yswgp9 # Helix Swap Contrac curl -X GET "https://sentry.lcd.injective.network/injective/exchange/v1beta1/spot/orders/$MARKET_ID/$SUBACCOUNT_ID" -H "accept: application/json" | jq . ``` -#### Subaccount Open Derivative Orders - -```bash -curl -X GET "https://sentry.lcd.injective.network/injective/exchange/v1beta1/derivative/orders/$MARKET_ID/$SUBACCOUNT_ID" -H "accept: application/json" | jq . -``` - -#### Subaccount Deposits - -```bash -curl -X GET "https://sentry.lcd.injective.network/injective/exchange/v1beta1/exchange/subaccountDeposits?subaccount_id=$SUBACCOUNT_ID" -H "accept: application/json" | jq . -``` - ## Wasm ### Store ```bash -injectived tx wasm store ./artifacts/grid-aarch64.wasm --from=sgt-account --gas=auto --gas-prices 500000000inj --gas-adjustment 1.3 --yes --output=json --node=https://testnet.sentry.tm.injective.network:443 --chain-id='injective-888' | jq . -``` - -### Instantiate - Derivative - -```bash -injectived tx wasm instantiate $CODE_ID '{"market_type": "derivative", "base_decimals": 18, "quote_decimals": 6, "market_id": "0x17ef48032cb24375ba7c2e39f384e56433bcab20cbee9a7357e4cba2eb00abe6", "small_order_threshold": "10.0"}' --label="inj-usdt-pgt" --admin sgt-account --from=sgt-account --gas=auto --gas-prices 500000000inj --gas-adjustment 1.3 --output=json --node=https://testnet.sentry.tm.injective.network:443 --chain-id='injective-888' -``` - -### Instantiate - Spot - -```bash -injectived tx wasm instantiate $CODE_ID '{"market_type": "spot", "base_decimals": 6, "quote_decimals": 6, "market_id": "0x42edf70cc37e155e9b9f178e04e18999bc8c404bd7b638cc4cbf41da8ef45a21", "valuation_market_id": "0xa508cb32923323679f29a032c70342c147c17d0145625922b0ef22e955c844c0", "small_order_threshold": "10.0"}' --label="qunt-inj-sgt" --admin sgt-account --from=sgt-account --gas=auto --gas-prices 500000000inj --gas-adjustment 1.3 --output=json --node=https://sentry.tm.injective.network:443 --chain-id='injective-1' -``` - -injectived tx wasm instantiate 685 '{"market_type": "spot", "base_decimals": 8, "quote_decimals": 6, "market_id": "0xb03ead807922111939d1b62121ae2956cf6f0a6b03dfdea8d9589c05b98f670f", "small_order_threshold": "10.0"}' --label="w-usdt-sgt" --admin sgt-account --from=sgt-account --gas=auto --gas-prices 500000000inj --gas-adjustment 1.3 --output=json --node=https://sentry.tm.injective.network:443 --chain-id='injective-1' - -### Execute - -#### Create Strategy - Trailing - -```bash -injectived tx wasm execute $CONTRACT_ADDRESS "{'create_strategy': {'subaccount_id': '$SUBACCOUNT_ID', 'bounds': ['1.0', 1.1], 'levels': 10, 'strategy_type': {'trailing_arithmetc': {'lower_trailing_bound': '0.5', 'upper_trailing_bound': '1.5'}}}}" --from=sgt-account --gas=auto --gas-prices 500000000inj --gas-adjustment 1.3 --output=json --yes --node=https://sentry.tm.injective.network:443 --chain-id='injective-1' | jq . +injectived tx wasm store ./artifacts/swap-aarch64.wasm --from=sgt-account --gas=auto --gas-prices 500000000inj --gas-adjustment 1.3 --yes --output=json --node=https://testnet.sentry.tm.injective.network:443 --chain-id='injective-888' | jq . ``` ### Query From 436d29d5f5097be1ea2508b827bda58de1564f06 Mon Sep 17 00:00:00 2001 From: maxrobot Date: Thu, 31 Oct 2024 20:29:14 +0000 Subject: [PATCH 8/9] chore: lint --- .github/workflows/rust.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml index 5ee8582..01aba82 100644 --- a/.github/workflows/rust.yaml +++ b/.github/workflows/rust.yaml @@ -123,13 +123,13 @@ jobs: - name: Run cargo fmt uses: actions-rs/cargo@v1 with: - toolchain: 1.74.0 + toolchain: 1.78.0 command: fmt args: --all -- --check - name: Run cargo clippy uses: actions-rs/cargo@v1 with: - toolchain: 1.74.0 + toolchain: 1.78.0 command: clippy args: --tests -- -D warnings From 851e199f78d698d47790ee27cdf57f110ce4e5bd Mon Sep 17 00:00:00 2001 From: jose Date: Fri, 8 Nov 2024 10:56:11 +0100 Subject: [PATCH 9/9] chore: github actions tool fix. chore: lints --- .github/workflows/rust.yaml | 2 +- contracts/swap/src/admin.rs | 55 +- contracts/swap/src/helpers.rs | 14 +- contracts/swap/src/state.rs | 24 +- ...ntegration_realistic_tests_min_quantity.rs | 591 ++++-------------- contracts/swap/src/testing/storage_tests.rs | 242 ++----- contracts/swap/src/testing/swap_tests.rs | 13 +- 7 files changed, 194 insertions(+), 747 deletions(-) diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml index 01aba82..fa9c5b6 100644 --- a/.github/workflows/rust.yaml +++ b/.github/workflows/rust.yaml @@ -62,7 +62,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - toolchain: 1.74.0 + toolchain: 1.78.0 args: --locked --tests env: LLVM_PROFILE_FILE: "swap-contract-%p-%m.profraw" diff --git a/contracts/swap/src/admin.rs b/contracts/swap/src/admin.rs index 1289aa6..90aa4ca 100644 --- a/contracts/swap/src/admin.rs +++ b/contracts/swap/src/admin.rs @@ -3,36 +3,22 @@ use crate::state::{remove_swap_route, store_swap_route, CONFIG}; use crate::types::{Config, SwapRoute}; use crate::ContractError; use crate::ContractError::CustomError; -use cosmwasm_std::{ - ensure, ensure_eq, Addr, Attribute, BankMsg, Coin, Deps, DepsMut, Env, Event, Response, - StdResult, -}; +use cosmwasm_std::{ensure, ensure_eq, Addr, Attribute, BankMsg, Coin, Deps, DepsMut, Env, Event, Response, StdResult}; use injective_cosmwasm::{InjectiveMsgWrapper, InjectiveQuerier, InjectiveQueryWrapper, MarketId}; use std::collections::HashSet; -pub fn save_config( - deps: DepsMut, - env: Env, - admin: Addr, - fee_recipient: FeeRecipient, -) -> StdResult<()> { +pub fn save_config(deps: DepsMut, env: Env, admin: Addr, fee_recipient: FeeRecipient) -> StdResult<()> { let fee_recipient = match fee_recipient { FeeRecipient::Address(addr) => addr, FeeRecipient::SwapContract => env.contract.address, }; - let config = Config { - fee_recipient, - admin, - }; + let config = Config { fee_recipient, admin }; config.to_owned().validate()?; CONFIG.save(deps.storage, &config) } -pub fn verify_sender_is_admin( - deps: Deps, - sender: &Addr, -) -> Result<(), ContractError> { +pub fn verify_sender_is_admin(deps: Deps, sender: &Addr) -> Result<(), ContractError> { let config = CONFIG.load(deps.storage)?; ensure_eq!(&config.admin, sender, ContractError::Unauthorized {}); Ok(()) @@ -57,10 +43,7 @@ pub fn update_config( FeeRecipient::Address(addr) => addr, FeeRecipient::SwapContract => env.contract.address, }; - updated_config_event_attrs.push(Attribute::new( - "fee_recipient", - config.fee_recipient.to_string(), - )); + updated_config_event_attrs.push(Attribute::new("fee_recipient", config.fee_recipient.to_string())); } CONFIG.save(deps.storage, &config)?; @@ -108,13 +91,7 @@ pub fn set_route( }); } - if route - .clone() - .into_iter() - .collect::>() - .len() - < route.len() - { + if route.clone().into_iter().collect::>().len() < route.len() { return Err(ContractError::CustomError { val: "Route cannot have duplicate steps!".to_string(), }); @@ -131,10 +108,7 @@ pub fn set_route( Ok(Response::new().add_attribute("method", "set_route")) } -fn verify_route_exists( - deps: Deps, - route: &SwapRoute, -) -> Result<(), ContractError> { +fn verify_route_exists(deps: Deps, route: &SwapRoute) -> Result<(), ContractError> { struct MarketDenom { quote_denom: String, base_denom: String, @@ -143,12 +117,9 @@ fn verify_route_exists( let querier = InjectiveQuerier::new(&deps.querier); for market_id in route.steps.iter() { - let market = querier - .query_spot_market(market_id)? - .market - .ok_or(CustomError { - val: format!("Market {} not found", market_id.as_str()).to_string(), - })?; + let market = querier.query_spot_market(market_id)?.market.ok_or(CustomError { + val: format!("Market {} not found", market_id.as_str()).to_string(), + })?; denoms.push(MarketDenom { quote_denom: market.quote_denom, @@ -164,15 +135,13 @@ fn verify_route_exists( } ); ensure!( - denoms.first().unwrap().quote_denom == route.source_denom - || denoms.first().unwrap().base_denom == route.source_denom, + denoms.first().unwrap().quote_denom == route.source_denom || denoms.first().unwrap().base_denom == route.source_denom, CustomError { val: "Source denom not found in first market".to_string() } ); ensure!( - denoms.last().unwrap().quote_denom == route.target_denom - || denoms.last().unwrap().base_denom == route.target_denom, + denoms.last().unwrap().quote_denom == route.target_denom || denoms.last().unwrap().base_denom == route.target_denom, CustomError { val: "Target denom not found in last market".to_string() } diff --git a/contracts/swap/src/helpers.rs b/contracts/swap/src/helpers.rs index 498bedc..a460955 100644 --- a/contracts/swap/src/helpers.rs +++ b/contracts/swap/src/helpers.rs @@ -10,10 +10,7 @@ pub fn i32_to_dec(source: i32) -> FPDecimal { FPDecimal::from(i128::from(source)) } -pub fn get_message_data( - response: &[SubMsg], - position: usize, -) -> &InjectiveMsgWrapper { +pub fn get_message_data(response: &[SubMsg], position: usize) -> &InjectiveMsgWrapper { let sth = match &response.get(position).unwrap().msg { CosmosMsg::Custom(msg) => msg, _ => panic!("No wrapped message found"), @@ -41,10 +38,7 @@ pub trait Scaled { impl Scaled for FPDecimal { fn scaled(self, digits: i32) -> Self { - self.to_owned() - * FPDecimal::from(10i128) - .pow(FPDecimal::from(digits as i128)) - .unwrap() + self.to_owned() * FPDecimal::from(10i128).pow(FPDecimal::from(digits as i128)).unwrap() } } @@ -55,9 +49,7 @@ pub fn dec_scale_factor() -> FPDecimal { type V100Config = Config; const V100CONFIG: Item = Item::new("config"); -pub fn handle_config_migration( - deps: DepsMut, -) -> Result { +pub fn handle_config_migration(deps: DepsMut) -> Result { let v100_config = V100CONFIG.load(deps.storage)?; let config = Config { diff --git a/contracts/swap/src/state.rs b/contracts/swap/src/state.rs index bebcb95..e423d20 100644 --- a/contracts/swap/src/state.rs +++ b/contracts/swap/src/state.rs @@ -22,17 +22,11 @@ pub fn store_swap_route(storage: &mut dyn Storage, route: &SwapRoute) -> StdResu SWAP_ROUTES.save(storage, key, route) } -pub fn read_swap_route( - storage: &dyn Storage, - source_denom: &str, - target_denom: &str, -) -> StdResult { +pub fn read_swap_route(storage: &dyn Storage, source_denom: &str, target_denom: &str) -> StdResult { let key = route_key(source_denom, target_denom); - SWAP_ROUTES.load(storage, key).map_err(|_| { - StdError::generic_err(format!( - "No swap route not found from {source_denom} to {target_denom}", - )) - }) + SWAP_ROUTES + .load(storage, key) + .map_err(|_| StdError::generic_err(format!("No swap route not found from {source_denom} to {target_denom}",))) } pub fn get_config(storage: &dyn Storage) -> StdResult { @@ -40,16 +34,10 @@ pub fn get_config(storage: &dyn Storage) -> StdResult { Ok(config) } -pub fn get_all_swap_routes( - storage: &dyn Storage, - start_after: Option<(String, String)>, - limit: Option, -) -> StdResult> { +pub fn get_all_swap_routes(storage: &dyn Storage, start_after: Option<(String, String)>, limit: Option) -> StdResult> { let limit = limit.unwrap_or(DEFAULT_LIMIT) as usize; - let start_bound = start_after - .as_ref() - .map(|(s, t)| Bound::inclusive((s.clone(), t.clone()))); + let start_bound = start_after.as_ref().map(|(s, t)| Bound::inclusive((s.clone(), t.clone()))); let routes = SWAP_ROUTES .range(storage, start_bound, None, Order::Ascending) diff --git a/contracts/swap/src/testing/integration_realistic_tests_min_quantity.rs b/contracts/swap/src/testing/integration_realistic_tests_min_quantity.rs index 6213738..3e2e961 100644 --- a/contracts/swap/src/testing/integration_realistic_tests_min_quantity.rs +++ b/contracts/swap/src/testing/integration_realistic_tests_min_quantity.rs @@ -1,6 +1,4 @@ -use injective_test_tube::{ - Account, Bank, Exchange, InjectiveTestApp, Module, RunnerResult, SigningAccount, Wasm, -}; +use injective_test_tube::{Account, Bank, Exchange, InjectiveTestApp, Module, RunnerResult, SigningAccount, Wasm}; use std::ops::Neg; use crate::helpers::Scaled; @@ -8,18 +6,13 @@ 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, + 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}; @@ -51,29 +44,15 @@ pub fn happy_path_two_hops_test(app: InjectiveTestApp, owner: SigningAccount, co &contr_addr, ETH, ATOM, - vec![ - spot_market_1_id.as_str().into(), - spot_market_2_id.as_str().into(), - ], + 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, - ); + 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); @@ -81,10 +60,7 @@ pub fn happy_path_two_hops_test(app: InjectiveTestApp, owner: SigningAccount, co let swapper = must_init_account_with_funds( &app, - &[ - str_coin(eth_to_swap, ETH, Decimals::Eighteen), - str_coin("1", INJ, Decimals::Eighteen), - ], + &[str_coin(eth_to_swap, ETH, Decimals::Eighteen), str_coin("1", INJ, Decimals::Eighteen)], ); let mut query_result: SwapEstimationResult = wasm @@ -118,18 +94,10 @@ pub fn happy_path_two_hops_test(app: InjectiveTestApp, owner: SigningAccount, co ]; // 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), - ); + 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" - ); + assert_eq!(contract_balances_before.len(), 1, "wrong number of denoms in contract balances"); wasm.execute( &contr_addr, @@ -145,11 +113,7 @@ pub fn happy_path_two_hops_test(app: InjectiveTestApp, owner: SigningAccount, co 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!(from_balance, FPDecimal::ZERO, "some of the original amount wasn't swapped"); assert!( to_balance >= expected_amount, @@ -161,11 +125,7 @@ pub fn happy_path_two_hops_test(app: InjectiveTestApp, owner: SigningAccount, co let max_diff = human_to_dec("0.1", Decimals::Six); assert!( - are_fpdecimals_approximately_equal( - expected_amount, - to_balance, - max_diff, - ), + 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()), @@ -173,16 +133,10 @@ pub fn happy_path_two_hops_test(app: InjectiveTestApp, owner: SigningAccount, co ); 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.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()); + 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, @@ -195,11 +149,7 @@ pub fn happy_path_two_hops_test(app: InjectiveTestApp, owner: SigningAccount, co 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, - ), + 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()), @@ -214,9 +164,7 @@ fn happy_path_two_hops_swap_eth_atom_realistic_values_self_relaying() { 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 _validator = app.get_first_validator_signing_account(INJ.to_string(), 1.2f64).unwrap(); let owner = must_init_account_with_funds( &app, &[ @@ -227,11 +175,7 @@ fn happy_path_two_hops_swap_eth_atom_realistic_values_self_relaying() { ], ); - 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)]); happy_path_two_hops_test(app, owner, contr_addr); } @@ -245,9 +189,7 @@ fn happy_path_two_hops_swap_inj_eth_realistic_values_self_relaying() { 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 _validator = app.get_first_validator_signing_account(INJ.to_string(), 1.2f64).unwrap(); let owner = must_init_account_with_funds( &app, &[ @@ -261,40 +203,22 @@ fn happy_path_two_hops_swap_inj_eth_realistic_values_self_relaying() { 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)], - ); + 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(), - ], + 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, - ); + 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); @@ -302,10 +226,7 @@ fn happy_path_two_hops_swap_inj_eth_realistic_values_self_relaying() { let swapper = must_init_account_with_funds( &app, - &[ - str_coin(inj_to_swap, INJ_2, Decimals::Eighteen), - str_coin("1", INJ, Decimals::Eighteen), - ], + &[str_coin(inj_to_swap, INJ_2, Decimals::Eighteen), str_coin("1", INJ, Decimals::Eighteen)], ); let mut query_result: SwapEstimationResult = wasm @@ -339,18 +260,10 @@ fn happy_path_two_hops_swap_inj_eth_realistic_values_self_relaying() { ]; // 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), - ); + 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" - ); + assert_eq!(contract_balances_before.len(), 1, "wrong number of denoms in contract balances"); wasm.execute( &contr_addr, @@ -366,11 +279,7 @@ fn happy_path_two_hops_swap_inj_eth_realistic_values_self_relaying() { 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_eq!(from_balance, FPDecimal::ZERO, "some of the original amount wasn't swapped"); assert!( to_balance >= expected_amount, @@ -382,11 +291,7 @@ fn happy_path_two_hops_swap_inj_eth_realistic_values_self_relaying() { let max_diff = human_to_dec("0.1", Decimals::Eighteen); assert!( - are_fpdecimals_approximately_equal( - expected_amount, - to_balance, - max_diff, - ), + 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()), @@ -394,16 +299,10 @@ fn happy_path_two_hops_swap_inj_eth_realistic_values_self_relaying() { ); 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.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()); + 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, @@ -416,11 +315,7 @@ fn happy_path_two_hops_swap_inj_eth_realistic_values_self_relaying() { 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, - ), + 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()), @@ -437,9 +332,7 @@ fn happy_path_two_hops_swap_inj_atom_realistic_values_self_relaying() { 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 _validator = app.get_first_validator_signing_account(INJ.to_string(), 1.2f64).unwrap(); let owner = must_init_account_with_funds( &app, &[ @@ -454,40 +347,22 @@ fn happy_path_two_hops_swap_inj_atom_realistic_values_self_relaying() { 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)], - ); + 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(), - ], + 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, - ); + 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); @@ -495,10 +370,7 @@ fn happy_path_two_hops_swap_inj_atom_realistic_values_self_relaying() { let swapper = must_init_account_with_funds( &app, - &[ - str_coin(inj_to_swap, INJ_2, Decimals::Eighteen), - str_coin("1", INJ, Decimals::Eighteen), - ], + &[str_coin(inj_to_swap, INJ_2, Decimals::Eighteen), str_coin("1", INJ, Decimals::Eighteen)], ); let mut query_result: SwapEstimationResult = wasm @@ -532,18 +404,10 @@ fn happy_path_two_hops_swap_inj_atom_realistic_values_self_relaying() { ]; // 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), - ); + 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" - ); + assert_eq!(contract_balances_before.len(), 1, "wrong number of denoms in contract balances"); wasm.execute( &contr_addr, @@ -559,11 +423,7 @@ fn happy_path_two_hops_swap_inj_atom_realistic_values_self_relaying() { 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_eq!(from_balance, FPDecimal::ZERO, "some of the original amount wasn't swapped"); assert!( to_balance >= expected_amount, @@ -575,11 +435,7 @@ fn happy_path_two_hops_swap_inj_atom_realistic_values_self_relaying() { let max_diff = human_to_dec("0.1", Decimals::Six); assert!( - are_fpdecimals_approximately_equal( - expected_amount, - to_balance, - max_diff, - ), + 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()), @@ -587,16 +443,10 @@ fn happy_path_two_hops_swap_inj_atom_realistic_values_self_relaying() { ); 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.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()); + 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, @@ -609,11 +459,7 @@ fn happy_path_two_hops_swap_inj_atom_realistic_values_self_relaying() { 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, - ), + 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()), @@ -629,9 +475,7 @@ fn it_executes_swap_between_markets_using_different_quote_assets_self_relaying() 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 _validator = app.get_first_validator_signing_account(INJ.to_string(), 1.2f64).unwrap(); let owner = must_init_account_with_funds( &app, @@ -649,10 +493,7 @@ fn it_executes_swap_between_markets_using_different_quote_assets_self_relaying() let contr_addr = init_self_relaying_contract_and_get_address( &wasm, &owner, - &[ - str_coin("10", USDC, Decimals::Six), - str_coin("500", USDT, Decimals::Six), - ], + &[str_coin("10", USDC, Decimals::Six), str_coin("500", USDT, Decimals::Six)], ); set_route_and_assert_success( &wasm, @@ -660,32 +501,18 @@ fn it_executes_swap_between_markets_using_different_quote_assets_self_relaying() &contr_addr, INJ_2, USDC, - vec![ - spot_market_1_id.as_str().into(), - spot_market_2_id.as_str().into(), - ], + 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_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 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"; @@ -720,18 +547,10 @@ fn it_executes_swap_between_markets_using_different_quote_assets_self_relaying() ]; // 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), - ); + 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" - ); + assert_eq!(contract_balances_before.len(), 2, "wrong number of denoms in contract balances"); wasm.execute( &contr_addr, @@ -747,11 +566,7 @@ fn it_executes_swap_between_markets_using_different_quote_assets_self_relaying() 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_eq!(from_balance, FPDecimal::ZERO, "some of the original amount wasn't swapped"); assert!( to_balance >= expected_amount, @@ -763,11 +578,7 @@ fn it_executes_swap_between_markets_using_different_quote_assets_self_relaying() let max_diff = human_to_dec("0.1", Decimals::Eighteen); assert!( - are_fpdecimals_approximately_equal( - expected_amount, - to_balance, - max_diff, - ), + 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()), @@ -775,17 +586,11 @@ fn it_executes_swap_between_markets_using_different_quote_assets_self_relaying() ); 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.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()); + 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, @@ -798,11 +603,7 @@ fn it_executes_swap_between_markets_using_different_quote_assets_self_relaying() 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, - ), + 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()), @@ -810,10 +611,8 @@ fn it_executes_swap_between_markets_using_different_quote_assets_self_relaying() ); // 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()); + 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, @@ -826,11 +625,7 @@ fn it_executes_swap_between_markets_using_different_quote_assets_self_relaying() 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, - ), + 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()), @@ -847,9 +642,7 @@ fn it_doesnt_lose_buffer_if_executed_multiple_times() { 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 _validator = app.get_first_validator_signing_account(INJ.to_string(), 1.2f64).unwrap(); let owner = must_init_account_with_funds( &app, @@ -864,11 +657,7 @@ fn it_doesnt_lose_buffer_if_executed_multiple_times() { 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)]); set_route_and_assert_success( &wasm, @@ -876,10 +665,7 @@ fn it_doesnt_lose_buffer_if_executed_multiple_times() { &contr_addr, ETH, ATOM, - vec![ - spot_market_1_id.as_str().into(), - spot_market_2_id.as_str().into(), - ], + vec![spot_market_1_id.as_str().into(), spot_market_2_id.as_str().into()], ); let trader1 = init_rich_account(&app); @@ -892,9 +678,7 @@ fn it_doesnt_lose_buffer_if_executed_multiple_times() { &app, &[ str_coin( - (FPDecimal::must_from_str(eth_to_swap) * FPDecimal::from(100u128)) - .to_string() - .as_str(), + (FPDecimal::must_from_str(eth_to_swap) * FPDecimal::from(100u128)).to_string().as_str(), ETH, Decimals::Eighteen, ), @@ -903,29 +687,14 @@ fn it_doesnt_lose_buffer_if_executed_multiple_times() { ); 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" - ); + 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, - ); + 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); @@ -944,16 +713,10 @@ fn it_doesnt_lose_buffer_if_executed_multiple_times() { } 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.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()); + 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, @@ -966,14 +729,12 @@ fn it_doesnt_lose_buffer_if_executed_multiple_times() { // 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()) + 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()) ); } @@ -987,8 +748,7 @@ fn it_doesnt_lose_buffer_if_executed_multiple_times() { */ #[ignore] #[test] -fn it_correctly_calculates_required_funds_when_querying_buy_with_minimum_buffer_and_realistic_values( -) { +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); @@ -996,9 +756,7 @@ fn it_correctly_calculates_required_funds_when_querying_buy_with_minimum_buffer_ 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 _validator = app.get_first_validator_signing_account(INJ.to_string(), 1.2f64).unwrap(); let owner = must_init_account_with_funds( &app, &[ @@ -1012,40 +770,22 @@ fn it_correctly_calculates_required_funds_when_querying_buy_with_minimum_buffer_ 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)], - ); + 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(), - ], + 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, - ); + 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); @@ -1053,10 +793,7 @@ fn it_correctly_calculates_required_funds_when_querying_buy_with_minimum_buffer_ let swapper = must_init_account_with_funds( &app, - &[ - str_coin(eth_to_swap, ETH, Decimals::Eighteen), - str_coin("1", INJ, Decimals::Eighteen), - ], + &[str_coin(eth_to_swap, ETH, Decimals::Eighteen), str_coin("1", INJ, Decimals::Eighteen)], ); let query_result: FPDecimal = wasm @@ -1077,11 +814,7 @@ fn it_correctly_calculates_required_funds_when_querying_buy_with_minimum_buffer_ ); 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" - ); + assert_eq!(contract_balances_before.len(), 1, "wrong number of denoms in contract balances"); wasm.execute( &contr_addr, @@ -1096,11 +829,7 @@ fn it_correctly_calculates_required_funds_when_querying_buy_with_minimum_buffer_ 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!(from_balance, FPDecimal::ZERO, "some of the original amount wasn't swapped"); assert_eq!( to_balance, human_to_dec("906.195", Decimals::Six), @@ -1108,11 +837,7 @@ fn it_correctly_calculates_required_funds_when_querying_buy_with_minimum_buffer_ ); 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.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); @@ -1125,10 +850,8 @@ fn it_correctly_calculates_required_funds_when_querying_buy_with_minimum_buffer_ 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 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 @@ -1148,8 +871,7 @@ fn it_correctly_calculates_required_funds_when_querying_buy_with_minimum_buffer_ */ #[ignore] #[test] -fn it_correctly_calculates_required_funds_when_executing_buy_with_minimum_buffer_and_realistic_values( -) { +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); @@ -1157,9 +879,7 @@ fn it_correctly_calculates_required_funds_when_executing_buy_with_minimum_buffer 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 _validator = app.get_first_validator_signing_account(INJ.to_string(), 1.2f64).unwrap(); let owner = must_init_account_with_funds( &app, &[ @@ -1174,40 +894,22 @@ fn it_correctly_calculates_required_funds_when_executing_buy_with_minimum_buffer 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)], - ); + 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(), - ], + 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, - ); + 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); @@ -1215,18 +917,11 @@ fn it_correctly_calculates_required_funds_when_executing_buy_with_minimum_buffer let swapper = must_init_account_with_funds( &app, - &[ - str_coin(eth_to_swap, ETH, Decimals::Eighteen), - str_coin("0.01", INJ, Decimals::Eighteen), - ], + &[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" - ); + assert_eq!(contract_balances_before.len(), 1, "wrong number of denoms in contract balances"); wasm.execute( &contr_addr, @@ -1241,11 +936,7 @@ fn it_correctly_calculates_required_funds_when_executing_buy_with_minimum_buffer 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!(from_balance, FPDecimal::ZERO, "some of the original amount wasn't swapped"); assert_eq!( to_balance, human_to_dec("906.195", Decimals::Six), @@ -1253,16 +944,10 @@ fn it_correctly_calculates_required_funds_when_executing_buy_with_minimum_buffer ); 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.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()); + 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, @@ -1275,11 +960,7 @@ fn it_correctly_calculates_required_funds_when_executing_buy_with_minimum_buffer 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, - ), + 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()), @@ -1296,9 +977,7 @@ fn it_returns_all_funds_if_there_is_not_enough_buffer_realistic_values() { 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 _validator = app.get_first_validator_signing_account(INJ.to_string(), 1.2f64).unwrap(); let owner = must_init_account_with_funds( &app, &[ @@ -1313,40 +992,22 @@ fn it_returns_all_funds_if_there_is_not_enough_buffer_realistic_values() { 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)], - ); + 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(), - ], + 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, - ); + 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); @@ -1354,10 +1015,7 @@ fn it_returns_all_funds_if_there_is_not_enough_buffer_realistic_values() { let swapper = must_init_account_with_funds( &app, - &[ - str_coin(eth_to_swap, ETH, Decimals::Eighteen), - str_coin("1", INJ, Decimals::Eighteen), - ], + &[str_coin(eth_to_swap, ETH, Decimals::Eighteen), str_coin("1", INJ, Decimals::Eighteen)], ); let query_result: RunnerResult = wasm.query( @@ -1372,19 +1030,12 @@ fn it_returns_all_funds_if_there_is_not_enough_buffer_realistic_values() { assert!(query_result.is_err(), "query should fail"); assert!( - query_result - .unwrap_err() - .to_string() - .contains("Swap amount too high"), + 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" - ); + assert_eq!(contract_balances_before.len(), 1, "wrong number of denoms in contract balances"); let execute_result = wasm.execute( &contr_addr, @@ -1406,18 +1057,10 @@ fn it_returns_all_funds_if_there_is_not_enough_buffer_realistic_values() { 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" - ); + 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.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/storage_tests.rs b/contracts/swap/src/testing/storage_tests.rs index 4e5dbdf..7690721 100644 --- a/contracts/swap/src/testing/storage_tests.rs +++ b/contracts/swap/src/testing/storage_tests.rs @@ -1,14 +1,10 @@ use cosmwasm_std::Addr; use crate::admin::{delete_route, set_route}; -use injective_cosmwasm::{ - inj_mock_deps, MarketId, OwnedDepsExt, TEST_MARKET_ID_1, TEST_MARKET_ID_2, TEST_MARKET_ID_3, -}; +use injective_cosmwasm::{inj_mock_deps, MarketId, OwnedDepsExt, TEST_MARKET_ID_1, TEST_MARKET_ID_2, TEST_MARKET_ID_3}; use crate::state::{read_swap_route, store_swap_route, CONFIG}; -use crate::testing::test_utils::{ - mock_deps_eth_inj, MultiplierQueryBehavior, TEST_CONTRACT_ADDR, TEST_USER_ADDR, -}; +use crate::testing::test_utils::{mock_deps_eth_inj, MultiplierQueryBehavior, TEST_CONTRACT_ADDR, TEST_USER_ADDR}; use crate::types::{Config, SwapRoute}; #[test] @@ -18,10 +14,7 @@ fn it_can_store_and_read_swap_route() { let target_denom = "inj"; let route = SwapRoute { - steps: vec![ - MarketId::unchecked(TEST_MARKET_ID_1), - MarketId::unchecked(TEST_MARKET_ID_2), - ], + steps: vec![MarketId::unchecked(TEST_MARKET_ID_1), MarketId::unchecked(TEST_MARKET_ID_2)], source_denom: source_denom.to_string(), target_denom: target_denom.to_string(), }; @@ -59,10 +52,7 @@ fn it_can_update_and_read_swap_route() { let new_target_denom = "inj"; let updated_route = SwapRoute { - steps: vec![ - MarketId::unchecked(TEST_MARKET_ID_1), - MarketId::unchecked(TEST_MARKET_ID_2), - ], + steps: vec![MarketId::unchecked(TEST_MARKET_ID_1), MarketId::unchecked(TEST_MARKET_ID_2)], source_denom: source_denom.to_string(), target_denom: new_target_denom.to_string(), }; @@ -78,18 +68,13 @@ fn owner_can_set_valid_route() { let mut deps = mock_deps_eth_inj(MultiplierQueryBehavior::Success); let source_denom = "eth".to_string(); let target_denom = "inj".to_string(); - let route = vec![ - MarketId::unchecked(TEST_MARKET_ID_1), - MarketId::unchecked(TEST_MARKET_ID_2), - ]; + let route = vec![MarketId::unchecked(TEST_MARKET_ID_1), MarketId::unchecked(TEST_MARKET_ID_2)]; let config = Config { fee_recipient: Addr::unchecked(TEST_USER_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 result = set_route( deps.as_mut(), @@ -102,25 +87,13 @@ fn owner_can_set_valid_route() { assert!(result.is_ok(), "result was not ok"); let response = result.unwrap(); - assert_eq!( - response.attributes[0].key, "method", - "method attribute was not set" - ); - assert_eq!( - response.attributes[0].value, "set_route", - "method attribute was not set" - ); + assert_eq!(response.attributes[0].key, "method", "method attribute was not set"); + assert_eq!(response.attributes[0].value, "set_route", "method attribute was not set"); let stored_route = read_swap_route(&deps.storage, &source_denom, &target_denom).unwrap(); assert_eq!(stored_route.steps, route, "route was not set correctly"); - assert_eq!( - stored_route.source_denom, source_denom, - "route was not set correctly" - ); - assert_eq!( - stored_route.target_denom, target_denom, - "route was not set correctly" - ); + assert_eq!(stored_route.source_denom, source_denom, "route was not set correctly"); + assert_eq!(stored_route.target_denom, target_denom, "route was not set correctly"); } #[test] @@ -128,18 +101,13 @@ fn owner_cannot_set_route_for_markets_using_target_denom_not_found_on_target_mar let mut deps = mock_deps_eth_inj(MultiplierQueryBehavior::Success); let source_denom = "eth".to_string(); let target_denom = "atom".to_string(); - let route = vec![ - MarketId::unchecked(TEST_MARKET_ID_1), - MarketId::unchecked(TEST_MARKET_ID_2), - ]; + let route = vec![MarketId::unchecked(TEST_MARKET_ID_1), MarketId::unchecked(TEST_MARKET_ID_2)]; let config = Config { fee_recipient: Addr::unchecked(TEST_USER_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 result = set_route( deps.as_mut(), @@ -151,10 +119,7 @@ fn owner_cannot_set_route_for_markets_using_target_denom_not_found_on_target_mar assert!(result.is_err(), "result was ok"); assert!( - result - .unwrap_err() - .to_string() - .contains("Target denom not found in last market"), + result.unwrap_err().to_string().contains("Target denom not found in last market"), "wrong error message" ); @@ -167,18 +132,13 @@ fn owner_cannot_set_route_for_markets_using_source_denom_not_present_on_source_m let mut deps = mock_deps_eth_inj(MultiplierQueryBehavior::Success); let source_denom = "atom".to_string(); let target_denom = "eth".to_string(); - let route = vec![ - MarketId::unchecked(TEST_MARKET_ID_1), - MarketId::unchecked(TEST_MARKET_ID_2), - ]; + let route = vec![MarketId::unchecked(TEST_MARKET_ID_1), MarketId::unchecked(TEST_MARKET_ID_2)]; let config = Config { fee_recipient: Addr::unchecked(TEST_USER_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 result = set_route( deps.as_mut(), @@ -190,10 +150,7 @@ fn owner_cannot_set_route_for_markets_using_source_denom_not_present_on_source_m assert!(result.is_err(), "result was ok"); assert!( - result - .unwrap_err() - .to_string() - .contains("Source denom not found in first market"), + result.unwrap_err().to_string().contains("Source denom not found in first market"), "wrong error message" ); @@ -212,9 +169,7 @@ fn owner_can_set_route_single_step_route() { fee_recipient: Addr::unchecked(TEST_USER_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 result = set_route( deps.as_mut(), @@ -227,25 +182,13 @@ fn owner_can_set_route_single_step_route() { assert!(result.is_ok(), "result was not ok"); let response = result.unwrap(); - assert_eq!( - response.attributes[0].key, "method", - "method attribute was not set" - ); - assert_eq!( - response.attributes[0].value, "set_route", - "method attribute was not set" - ); + assert_eq!(response.attributes[0].key, "method", "method attribute was not set"); + assert_eq!(response.attributes[0].value, "set_route", "method attribute was not set"); let stored_route = read_swap_route(&deps.storage, &source_denom, &target_denom).unwrap(); assert_eq!(stored_route.steps, route, "route was not stored correctly"); - assert_eq!( - stored_route.source_denom, source_denom, - "source_denom was not stored correctly" - ); - assert_eq!( - stored_route.target_denom, target_denom, - "target_denom was not stored correctly" - ); + assert_eq!(stored_route.source_denom, source_denom, "source_denom was not stored correctly"); + assert_eq!(stored_route.target_denom, target_denom, "target_denom was not stored correctly"); } #[test] @@ -259,9 +202,7 @@ fn owner_can_set_route_single_step_route_with_reverted_denoms() { fee_recipient: Addr::unchecked(TEST_USER_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 result = set_route( deps.as_mut(), @@ -274,25 +215,13 @@ fn owner_can_set_route_single_step_route_with_reverted_denoms() { assert!(result.is_ok(), "result was not ok"); let response = result.unwrap(); - assert_eq!( - response.attributes[0].key, "method", - "method attribute was not set" - ); - assert_eq!( - response.attributes[0].value, "set_route", - "method attribute was not set" - ); + assert_eq!(response.attributes[0].key, "method", "method attribute was not set"); + assert_eq!(response.attributes[0].value, "set_route", "method attribute was not set"); let stored_route = read_swap_route(&deps.storage, &source_denom, &target_denom).unwrap(); assert_eq!(stored_route.steps, route, "route was not stored correctly"); - assert_eq!( - stored_route.source_denom, source_denom, - "source_denom was not stored correctly" - ); - assert_eq!( - stored_route.target_denom, target_denom, - "target_denom was not stored correctly" - ); + assert_eq!(stored_route.source_denom, source_denom, "source_denom was not stored correctly"); + assert_eq!(stored_route.target_denom, target_denom, "target_denom was not stored correctly"); } #[test] @@ -300,19 +229,14 @@ fn it_returns_error_when_setting_route_for_the_same_denom_as_target_and_source() let mut deps = mock_deps_eth_inj(MultiplierQueryBehavior::Success); let source_denom = "eth".to_string(); let target_denom = "eth".to_string(); - let route = vec![ - MarketId::unchecked(TEST_MARKET_ID_1), - MarketId::unchecked(TEST_MARKET_ID_2), - ]; + let route = vec![MarketId::unchecked(TEST_MARKET_ID_1), MarketId::unchecked(TEST_MARKET_ID_2)]; let config = Config { fee_recipient: Addr::unchecked(TEST_USER_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 result = set_route( deps.as_mut(), @@ -322,10 +246,7 @@ fn it_returns_error_when_setting_route_for_the_same_denom_as_target_and_source() route, ); - assert!( - result.is_err(), - "Could set a route with the same denom being source and target!" - ); + assert!(result.is_err(), "Could set a route with the same denom being source and target!"); assert!( result .unwrap_err() @@ -335,10 +256,7 @@ fn it_returns_error_when_setting_route_for_the_same_denom_as_target_and_source() ); let stored_route = read_swap_route(&deps.storage, &source_denom, &target_denom); - assert!( - stored_route.is_err(), - "Could read a route with the same denom being source and target!" - ); + assert!(stored_route.is_err(), "Could read a route with the same denom being source and target!"); } #[test] @@ -353,9 +271,7 @@ fn it_returns_error_when_setting_route_with_nonexistent_market_id() { 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 result = set_route( deps.as_mut(), @@ -369,17 +285,12 @@ fn it_returns_error_when_setting_route_with_nonexistent_market_id() { let err_result = result.unwrap_err(); assert!( - err_result - .to_string() - .contains(&format!("Market {TEST_MARKET_ID_3} not found")), + err_result.to_string().contains(&format!("Market {TEST_MARKET_ID_3} not found")), "wrong error message" ); let stored_route = read_swap_route(&deps.storage, &source_denom, &target_denom); - assert!( - stored_route.is_err(), - "Could read a route for non-existent market" - ); + assert!(stored_route.is_err(), "Could read a route for non-existent market"); } #[test] @@ -394,9 +305,7 @@ fn it_returns_error_when_setting_route_with_no_market_ids() { 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 result = set_route( deps.as_mut(), @@ -408,18 +317,12 @@ fn it_returns_error_when_setting_route_with_no_market_ids() { assert!(result.is_err(), "Could set a route without any steps"); assert!( - result - .unwrap_err() - .to_string() - .contains("Route must have at least one step"), + result.unwrap_err().to_string().contains("Route must have at least one step"), "wrong error message" ); let stored_route = read_swap_route(&deps.storage, &source_denom, &target_denom); - assert!( - stored_route.is_err(), - "Could read a route without any steps" - ); + assert!(stored_route.is_err(), "Could read a route without any steps"); } #[test] @@ -427,19 +330,14 @@ fn it_returns_error_when_setting_route_with_duplicated_market_ids() { let mut deps = inj_mock_deps(|_| {}); let source_denom = "eth".to_string(); let target_denom = "usdt".to_string(); - let route = vec![ - MarketId::unchecked(TEST_MARKET_ID_1), - MarketId::unchecked(TEST_MARKET_ID_1), - ]; + let route = vec![MarketId::unchecked(TEST_MARKET_ID_1), MarketId::unchecked(TEST_MARKET_ID_1)]; let config = Config { fee_recipient: Addr::unchecked(TEST_USER_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 result = set_route( deps.as_mut(), @@ -449,23 +347,14 @@ fn it_returns_error_when_setting_route_with_duplicated_market_ids() { route, ); + assert!(result.is_err(), "Could set a route that begins and ends with the same market"); assert!( - result.is_err(), - "Could set a route that begins and ends with the same market" - ); - assert!( - result - .unwrap_err() - .to_string() - .contains("Route cannot have duplicate steps"), + result.unwrap_err().to_string().contains("Route cannot have duplicate steps"), "wrong error message" ); let stored_route = read_swap_route(&deps.storage, &source_denom, &target_denom); - assert!( - stored_route.is_err(), - "Could read a route that begins and ends with the same market" - ); + assert!(stored_route.is_err(), "Could read a route that begins and ends with the same market"); } #[test] @@ -473,18 +362,13 @@ fn it_returns_error_if_non_admin_tries_to_set_route() { let mut deps = mock_deps_eth_inj(MultiplierQueryBehavior::Success); let source_denom = "eth".to_string(); let target_denom = "inj".to_string(); - let route = vec![ - MarketId::unchecked(TEST_MARKET_ID_1), - MarketId::unchecked(TEST_MARKET_ID_2), - ]; + let route = vec![MarketId::unchecked(TEST_MARKET_ID_1), MarketId::unchecked(TEST_MARKET_ID_2)]; let config = Config { fee_recipient: Addr::unchecked(TEST_USER_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 result = set_route( deps.as_mut(), @@ -495,16 +379,11 @@ fn it_returns_error_if_non_admin_tries_to_set_route() { ); assert!(result.is_err(), "expected error"); - assert!( - result.unwrap_err().to_string().contains("Unauthorized"), - "wrong error message" - ); + assert!(result.unwrap_err().to_string().contains("Unauthorized"), "wrong error message"); let stored_route = read_swap_route(&deps.storage, &source_denom, &target_denom).unwrap_err(); assert!( - stored_route - .to_string() - .contains("No swap route not found from eth to inj"), + stored_route.to_string().contains("No swap route not found from eth to inj"), "wrong error message" ); } @@ -514,18 +393,13 @@ fn it_allows_admint_to_delete_existing_route() { let mut deps = mock_deps_eth_inj(MultiplierQueryBehavior::Success); let source_denom = "eth".to_string(); let target_denom = "inj".to_string(); - let route = vec![ - MarketId::unchecked(TEST_MARKET_ID_1), - MarketId::unchecked(TEST_MARKET_ID_2), - ]; + let route = vec![MarketId::unchecked(TEST_MARKET_ID_1), MarketId::unchecked(TEST_MARKET_ID_2)]; let config = Config { fee_recipient: Addr::unchecked(TEST_USER_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 set_result = set_route( deps.as_mut(), @@ -548,9 +422,7 @@ fn it_allows_admint_to_delete_existing_route() { let stored_route = read_swap_route(&deps.storage, &source_denom, &target_denom).unwrap_err(); assert!( - stored_route - .to_string() - .contains("No swap route not found from eth to inj"), + stored_route.to_string().contains("No swap route not found from eth to inj"), "route was not deleted and could be read" ); } @@ -560,18 +432,13 @@ fn it_doesnt_fail_if_admin_deletes_non_existent_route() { let mut deps = mock_deps_eth_inj(MultiplierQueryBehavior::Success); let source_denom = "eth".to_string(); let target_denom = "inj".to_string(); - let route = vec![ - MarketId::unchecked(TEST_MARKET_ID_1), - MarketId::unchecked(TEST_MARKET_ID_2), - ]; + let route = vec![MarketId::unchecked(TEST_MARKET_ID_1), MarketId::unchecked(TEST_MARKET_ID_2)]; let config = Config { fee_recipient: Addr::unchecked(TEST_USER_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 set_result = set_route( deps.as_mut(), @@ -601,18 +468,13 @@ fn it_returns_error_if_non_admin_tries_to_delete_route() { let mut deps = mock_deps_eth_inj(MultiplierQueryBehavior::Success); let source_denom = "eth".to_string(); let target_denom = "inj".to_string(); - let route = vec![ - MarketId::unchecked(TEST_MARKET_ID_1), - MarketId::unchecked(TEST_MARKET_ID_2), - ]; + let route = vec![MarketId::unchecked(TEST_MARKET_ID_1), MarketId::unchecked(TEST_MARKET_ID_2)]; let config = Config { fee_recipient: Addr::unchecked(TEST_USER_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 set_result = set_route( deps.as_mut(), diff --git a/contracts/swap/src/testing/swap_tests.rs b/contracts/swap/src/testing/swap_tests.rs index e35baae..19b8c7b 100644 --- a/contracts/swap/src/testing/swap_tests.rs +++ b/contracts/swap/src/testing/swap_tests.rs @@ -6,9 +6,7 @@ use injective_cosmwasm::{MarketId, OwnedDepsExt, TEST_MARKET_ID_1, TEST_MARKET_I use crate::admin::set_route; use crate::queries::estimate_single_swap_execution; use crate::state::CONFIG; -use crate::testing::test_utils::{ - mock_deps_eth_inj, str_coin, Decimals, MultiplierQueryBehavior, TEST_USER_ADDR, -}; +use crate::testing::test_utils::{mock_deps_eth_inj, str_coin, Decimals, MultiplierQueryBehavior, TEST_USER_ADDR}; use crate::types::{Config, FPCoin, SwapEstimationAmount}; #[test] @@ -21,9 +19,7 @@ fn it_reverts_if_atomic_fee_multiplier_query_fails() { fee_recipient: Addr::unchecked(TEST_USER_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"); set_route( deps.as_mut_deps(), @@ -44,10 +40,7 @@ fn it_reverts_if_atomic_fee_multiplier_query_fails() { assert!(response_1.is_err(), "should have failed"); assert!( - response_1 - .unwrap_err() - .to_string() - .contains("Querier system error: Unknown system error"), + response_1.unwrap_err().to_string().contains("Querier system error: Unknown system error"), "wrong error message" ); }

-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 zν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