From 95b95c490c65d9e7fd8fb81136f90c794d034942 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Thu, 11 Apr 2024 14:41:01 +0400 Subject: [PATCH] feat(maker): add fee swapper contract (maker) (#14) * WIP: add custom maker for Osmosis deployment * WIP: maker first version * WIP: add maker tests * WIP: preapare test suite * covering with tests * more tests * use spot price query instead of twap * add test-tube based tests for maker * linter happy * bump deps * add migration from dummy maker * fix comment * bump main crate version --- Cargo.lock | 429 +++++++------ Cargo.toml | 3 +- contracts/maker/Cargo.toml | 34 ++ contracts/maker/src/error.rs | 63 ++ contracts/maker/src/execute.rs | 487 +++++++++++++++ contracts/maker/src/instantiate.rs | 47 ++ contracts/maker/src/lib.rs | 8 + contracts/maker/src/migrate.rs | 56 ++ contracts/maker/src/query.rs | 85 +++ contracts/maker/src/reply.rs | 38 ++ contracts/maker/src/state.rs | 21 + contracts/maker/src/utils.rs | 148 +++++ contracts/maker/tests/common/helper.rs | 460 ++++++++++++++ contracts/maker/tests/common/mod.rs | 2 + contracts/maker/tests/common/osmosis_ext.rs | 346 +++++++++++ contracts/maker/tests/maker_integration.rs | 567 ++++++++++++++++++ e2e_tests/Cargo.toml | 7 +- e2e_tests/contracts/README.md | 3 +- e2e_tests/contracts/astro_satellite.wasm | Bin 0 -> 314530 bytes e2e_tests/src/helper.rs | 68 ++- e2e_tests/tests/maker_e2e_testing.rs | 106 ++++ .../{e2e_testing.rs => pcl_e2e_testing.rs} | 82 +-- packages/astroport_on_osmosis/Cargo.toml | 2 +- packages/astroport_on_osmosis/src/lib.rs | 1 + packages/astroport_on_osmosis/src/maker.rs | 117 ++++ 25 files changed, 2914 insertions(+), 266 deletions(-) create mode 100644 contracts/maker/Cargo.toml create mode 100644 contracts/maker/src/error.rs create mode 100644 contracts/maker/src/execute.rs create mode 100644 contracts/maker/src/instantiate.rs create mode 100644 contracts/maker/src/lib.rs create mode 100644 contracts/maker/src/migrate.rs create mode 100644 contracts/maker/src/query.rs create mode 100644 contracts/maker/src/reply.rs create mode 100644 contracts/maker/src/state.rs create mode 100644 contracts/maker/src/utils.rs create mode 100644 contracts/maker/tests/common/helper.rs create mode 100644 contracts/maker/tests/common/mod.rs create mode 100644 contracts/maker/tests/common/osmosis_ext.rs create mode 100644 contracts/maker/tests/maker_integration.rs create mode 100644 e2e_tests/contracts/astro_satellite.wasm create mode 100644 e2e_tests/tests/maker_e2e_testing.rs rename e2e_tests/tests/{e2e_testing.rs => pcl_e2e_testing.rs} (73%) create mode 100644 packages/astroport_on_osmosis/src/maker.rs diff --git a/Cargo.lock b/Cargo.lock index 1dfa653..97221fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,18 +30,29 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "anyhow" -version = "1.0.79" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" + +[[package]] +name = "astro-satellite-package" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "893363819104a2a4685d99f19e35e5d81102fb782e622619ac643e54ff65d638" +dependencies = [ + "astroport-governance", + "cosmwasm-schema", + "cosmwasm-std", +] [[package]] name = "astroport" @@ -60,8 +71,8 @@ dependencies = [ [[package]] name = "astroport" -version = "3.11.1" -source = "git+https://github.com/astroport-fi/astroport-core#7a09556a76c56a93b655c76adc2ea08e89d6f40d" +version = "3.12.1" +source = "git+https://github.com/astroport-fi/astroport-core#148930eaedf3cacf0c31844045891835c0b29a95" dependencies = [ "astroport-circular-buffer", "cosmwasm-schema", @@ -78,7 +89,7 @@ dependencies = [ [[package]] name = "astroport-circular-buffer" version = "0.1.0" -source = "git+https://github.com/astroport-fi/astroport-core#7a09556a76c56a93b655c76adc2ea08e89d6f40d" +source = "git+https://github.com/astroport-fi/astroport-core#148930eaedf3cacf0c31844045891835c0b29a95" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -89,9 +100,9 @@ dependencies = [ [[package]] name = "astroport-factory" version = "1.7.0" -source = "git+https://github.com/astroport-fi/astroport-core#7a09556a76c56a93b655c76adc2ea08e89d6f40d" +source = "git+https://github.com/astroport-fi/astroport-core#148930eaedf3cacf0c31844045891835c0b29a95" dependencies = [ - "astroport 3.11.1", + "astroport 3.12.1", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -107,7 +118,42 @@ name = "astroport-factory-osmosis" version = "1.0.0" dependencies = [ "anyhow", - "astroport 3.11.1", + "astroport 3.12.1", + "astroport-on-osmosis", + "astroport-pcl-osmo", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", + "cw-storage-plus 0.15.1", + "cw-utils 1.0.3", + "cw2 1.1.2", + "itertools 0.12.1", + "osmosis-std", + "thiserror", +] + +[[package]] +name = "astroport-governance" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72806ace350e81c4e1cab7e275ef91f05bad830275d697d67ad1bd4acc6f016d" +dependencies = [ + "astroport 2.9.5", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.15.1", + "cw20 0.15.1", +] + +[[package]] +name = "astroport-maker-osmosis" +version = "1.0.0" +dependencies = [ + "anyhow", + "astro-satellite-package", + "astroport 3.12.1", + "astroport-factory-osmosis", + "astroport-native-coin-registry", "astroport-on-osmosis", "astroport-pcl-osmo", "cosmwasm-schema", @@ -116,6 +162,7 @@ dependencies = [ "cw-storage-plus 0.15.1", "cw-utils 1.0.3", "cw2 1.1.2", + "derivative", "itertools 0.12.1", "osmosis-std", "thiserror", @@ -138,9 +185,9 @@ dependencies = [ [[package]] name = "astroport-on-osmosis" -version = "1.0.1" +version = "1.1.0" dependencies = [ - "astroport 3.11.1", + "astroport 3.12.1", "cosmwasm-schema", "cosmwasm-std", ] @@ -150,22 +197,23 @@ name = "astroport-osmo-e2e-tests" version = "0.1.0" dependencies = [ "anyhow", - "astroport 3.11.1", + "astro-satellite-package", + "astroport 3.12.1", "astroport-on-osmosis", "cosmwasm-std", "osmosis-std", "osmosis-test-tube", "serde", "serde_json", - "test-tube 0.4.0", + "test-tube", ] [[package]] name = "astroport-pcl-common" version = "1.1.0" -source = "git+https://github.com/astroport-fi/astroport-core#7a09556a76c56a93b655c76adc2ea08e89d6f40d" +source = "git+https://github.com/astroport-fi/astroport-core#148930eaedf3cacf0c31844045891835c0b29a95" dependencies = [ - "astroport 3.11.1", + "astroport 3.12.1", "astroport-factory", "cosmwasm-schema", "cosmwasm-std", @@ -180,7 +228,7 @@ name = "astroport-pcl-osmo" version = "1.0.1" dependencies = [ "anyhow", - "astroport 3.11.1", + "astroport 3.12.1", "astroport-circular-buffer", "astroport-factory-osmosis", "astroport-native-coin-registry", @@ -201,13 +249,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.77" +version = "0.1.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +checksum = "461abc97219de0eaaf81fe3ef974a540158f3d079c2ab200f891f1a2ef201e85" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.53", ] [[package]] @@ -218,9 +266,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "95d8e92cac0961e91dbd517496b00f7e9b92363dbe6d42c3198268323798860c" dependencies = [ "addr2line", "cc", @@ -261,7 +309,7 @@ version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -274,7 +322,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.49", + "syn 2.0.53", "which", ] @@ -302,9 +350,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "block-buffer" @@ -332,18 +380,18 @@ checksum = "56953345e39537a3e18bdaeba4cb0c58a78c1f61f361dc0fa7c5c7340ae87c5f" [[package]] name = "bs58" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5353f36341f7451062466f0b755b96ac3a9547e4d7f6b70d603fc721a7d7896" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" dependencies = [ "sha2 0.10.8", ] [[package]] name = "bumpalo" -version = "3.15.0" +version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32a994c2b3ca201d9b263612a374263f05e7adde37c4707f693dcd375076d1f" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "byteorder" @@ -362,12 +410,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.83" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" [[package]] name = "cexpr" @@ -386,9 +431,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.34" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" dependencies = [ "num-traits", ] @@ -625,9 +670,9 @@ dependencies = [ [[package]] name = "cw-multi-test" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fff029689ae89127cf6d7655809a68d712f3edbdb9686c70b018ba438b26ca" +checksum = "cc392a5cb7e778e3f90adbf7faa43c4db7f35b6623224b08886d796718edb875" dependencies = [ "anyhow", "bech32", @@ -834,9 +879,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "ecdsa" @@ -1101,9 +1146,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" dependencies = [ "bytes", "fnv", @@ -1135,9 +1180,9 @@ checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "hermit-abi" -version = "0.3.6" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd5256b483761cd23699d0da46cc6fd2ee3be420bbe6d020ae4a091e70b7e9fd" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -1165,9 +1210,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", @@ -1253,9 +1298,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" -version = "2.2.3" +version = "2.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" +checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" dependencies = [ "equivalent", "hashbrown 0.14.3", @@ -1302,9 +1347,9 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -1343,12 +1388,12 @@ checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libloading" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-sys 0.48.0", + "windows-targets 0.52.4", ] [[package]] @@ -1359,9 +1404,9 @@ checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" @@ -1392,9 +1437,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", @@ -1464,9 +1509,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[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" @@ -1476,9 +1521,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "osmosis-std" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e87adf61f03306474ce79ab322d52dfff6b0bcf3aed1e12d8864ac0400dec1bf" +checksum = "8641c376f01f5af329dc2a34e4f5527eaeb0bde18cda8d86fed958d04c86159c" dependencies = [ "chrono", "cosmwasm-std", @@ -1505,9 +1550,9 @@ dependencies = [ [[package]] name = "osmosis-test-tube" -version = "21.0.0" +version = "22.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a528c942d25d3159634f77953ca0e16c5a450fc44578ad922320128e4025fd" +checksum = "a082b97136d15470a37aa758f227c865594590b69d74721248ed5adf59bf1ca2" dependencies = [ "base64", "bindgen", @@ -1517,7 +1562,7 @@ dependencies = [ "prost 0.12.3", "serde", "serde_json", - "test-tube 0.3.0", + "test-tube", "thiserror", ] @@ -1529,9 +1574,9 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "peg" -version = "0.7.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c0b841ea54f523f7aa556956fbd293bcbe06f2e67d2eb732b7278aaf1d166a" +checksum = "400bcab7d219c38abf8bd7cc2054eb9bbbd4312d66f6a5557d572a203f646f61" dependencies = [ "peg-macros", "peg-runtime", @@ -1539,9 +1584,9 @@ dependencies = [ [[package]] name = "peg-macros" -version = "0.7.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aa52829b8decbef693af90202711348ab001456803ba2a98eb4ec8fb70844c" +checksum = "46e61cce859b76d19090f62da50a9fe92bab7c2a5f09e183763559a2ac392c90" dependencies = [ "peg-runtime", "proc-macro2", @@ -1550,9 +1595,9 @@ dependencies = [ [[package]] name = "peg-runtime" -version = "0.7.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c719dcf55f09a3a7e764c6649ab594c18a177e3599c467983cdf644bfc0a4088" +checksum = "36bae92c60fa2398ce4678b98b2c4b5a7c61099961ca1fa305aec04a9ad28922" [[package]] name = "percent-encoding" @@ -1562,22 +1607,22 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.53", ] [[package]] @@ -1608,6 +1653,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "prettyplease" version = "0.2.16" @@ -1615,14 +1666,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" dependencies = [ "proc-macro2", - "syn 2.0.49", + "syn 2.0.53", ] [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -1670,7 +1721,7 @@ dependencies = [ "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.53", ] [[package]] @@ -1709,6 +1760,27 @@ dependencies = [ "proc-macro2", ] +[[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.5.1" @@ -1738,9 +1810,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", @@ -1755,9 +1827,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.11.24" +version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "base64", "bytes", @@ -1806,16 +1878,17 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.7" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", + "cfg-if", "getrandom", "libc", "spin", "untrusted", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1841,11 +1914,11 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys", @@ -1897,9 +1970,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "same-file" @@ -1992,15 +2065,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" [[package]] name = "serde" -version = "1.0.196" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] @@ -2034,13 +2107,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.196" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.53", ] [[package]] @@ -2056,9 +2129,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.113" +version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" dependencies = [ "itoa", "ryu", @@ -2073,7 +2146,7 @@ checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.53", ] [[package]] @@ -2139,12 +2212,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -2203,9 +2276,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.49" +version = "2.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496" +checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" dependencies = [ "proc-macro2", "quote", @@ -2241,9 +2314,9 @@ dependencies = [ [[package]] name = "tendermint" -version = "0.34.0" +version = "0.34.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc2294fa667c8b548ee27a9ba59115472d0a09c2ba255771092a7f1dcf03a789" +checksum = "15ab8f0a25d0d2ad49ac615da054d6a76aa6603ff95f7d18bafdd34450a1a04b" dependencies = [ "bytes", "digest 0.10.7", @@ -2272,9 +2345,9 @@ dependencies = [ [[package]] name = "tendermint-config" -version = "0.34.0" +version = "0.34.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a25dbe8b953e80f3d61789fbdb83bf9ad6c0ef16df5ca6546f49912542cc137" +checksum = "e1a02da769166e2052cd537b1a97c78017632c2d9e19266367b27e73910434fc" dependencies = [ "flex-error", "serde", @@ -2286,9 +2359,9 @@ dependencies = [ [[package]] name = "tendermint-proto" -version = "0.34.0" +version = "0.34.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc728a4f9e891d71adf66af6ecaece146f9c7a11312288a3107b3e1d6979aaf" +checksum = "b797dd3d2beaaee91d2f065e7bdf239dc8d80bba4a183a288bc1279dd5a69a1e" dependencies = [ "bytes", "flex-error", @@ -2304,9 +2377,9 @@ dependencies = [ [[package]] name = "tendermint-rpc" -version = "0.34.0" +version = "0.34.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbf0a4753b46a190f367337e0163d0b552a2674a6bac54e74f9f2cdcde2969b" +checksum = "71afae8bb5f6b14ed48d4e1316a643b6c2c3cbad114f510be77b4ed20b7b3e42" dependencies = [ "async-trait", "bytes", @@ -2315,6 +2388,7 @@ dependencies = [ "getrandom", "peg", "pin-project", + "rand", "reqwest", "semver", "serde", @@ -2336,9 +2410,9 @@ dependencies = [ [[package]] name = "test-tube" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17f30e7fea966bde5f9933a4cb2db79dd272115ea19d1656da2aac7ce0700fa" +checksum = "09184c7655b2bdaf4517b06141a2e4c44360904f2706a05b24c831bd97ad1db6" dependencies = [ "base64", "cosmrs", @@ -2350,39 +2424,24 @@ dependencies = [ "thiserror", ] -[[package]] -name = "test-tube" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f330a3c269a6abcda205d272d600be1458df82bf9ad40c0d2244ffa643f76b2a" -dependencies = [ - "base64", - "cosmrs", - "cosmwasm-std", - "prost 0.12.3", - "serde", - "serde_json", - "thiserror", -] - [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.53", ] [[package]] @@ -2455,7 +2514,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.53", ] [[package]] @@ -2554,9 +2613,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] @@ -2580,9 +2639,9 @@ dependencies = [ [[package]] name = "uuid" -version = "0.8.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" [[package]] name = "version_check" @@ -2592,9 +2651,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -2617,9 +2676,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2627,24 +2686,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.53", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -2654,9 +2713,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2664,28 +2723,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.53", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", @@ -2749,7 +2808,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.4", ] [[package]] @@ -2769,17 +2828,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", ] [[package]] @@ -2790,9 +2849,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] name = "windows_aarch64_msvc" @@ -2802,9 +2861,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" @@ -2814,9 +2873,9 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" @@ -2826,9 +2885,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" @@ -2838,9 +2897,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] name = "windows_x86_64_gnullvm" @@ -2850,9 +2909,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" @@ -2862,9 +2921,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "winreg" @@ -2893,5 +2952,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.53", ] diff --git a/Cargo.toml b/Cargo.toml index 726a245..7f34ede 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,4 +23,5 @@ astroport = { git = "https://github.com/astroport-fi/astroport-core", version = astroport-pcl-common = { git = "https://github.com/astroport-fi/astroport-core", version = "1.1.0" } astroport-circular-buffer = { git = "https://github.com/astroport-fi/astroport-core", version = "0.1.0" } astroport-native-coin-registry = "1.0.1" -osmosis-std = "0.21" \ No newline at end of file +osmosis-std = "0.22" +itertools = "0.12" \ No newline at end of file diff --git a/contracts/maker/Cargo.toml b/contracts/maker/Cargo.toml new file mode 100644 index 0000000..e92e3ff --- /dev/null +++ b/contracts/maker/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "astroport-maker-osmosis" +version = "1.0.0" +authors = ["Astroport"] +edition = "2021" +description = "Astroport maker contract for Osmosis" +license = "GPL-3.0-only" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +library = [] + +[dependencies] +cosmwasm-std = { version = "1", features = ["cosmwasm_1_1"] } +cosmwasm-schema = "1" +cw-storage-plus = "0.15" +osmosis-std.workspace = true +cw-utils = "1" +cw2 = "1" +astroport.workspace = true +astroport-on-osmosis = { path = "../../packages/astroport_on_osmosis", version = "1" } +astro-satellite-package = "1" +thiserror = "1.0" +itertools.workspace = true + +[dev-dependencies] +cw-multi-test = { version = "0.20.0", features = ["cosmwasm_1_1"] } +anyhow = "1" +derivative = "2" +astroport-native-coin-registry = { workspace = true } +astroport-factory = { package = "astroport-factory-osmosis", path = "../factory" } +astroport-pcl-osmo = { path = "../pair_concentrated" } \ No newline at end of file diff --git a/contracts/maker/src/error.rs b/contracts/maker/src/error.rs new file mode 100644 index 0000000..e5e5ea8 --- /dev/null +++ b/contracts/maker/src/error.rs @@ -0,0 +1,63 @@ +use cosmwasm_std::{CheckedMultiplyRatioError, OverflowError, StdError}; +use cw2::VersionError; +use cw_utils::PaymentError; +use thiserror::Error; + +use astroport_on_osmosis::maker::{PoolRoute, MAX_ALLOWED_SPREAD, MAX_SWAPS_DEPTH}; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + OverflowError(#[from] OverflowError), + + #[error("{0}")] + PaymentError(#[from] PaymentError), + + #[error("{0}")] + CheckedMultiplyRatioError(#[from] CheckedMultiplyRatioError), + + #[error("{0}")] + VersionError(#[from] VersionError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Max spread too high. Max allowed: {MAX_ALLOWED_SPREAD}")] + MaxSpreadTooHigh {}, + + #[error("Incorrect cooldown. Min: {min}, Max: {max}")] + IncorrectCooldown { min: u64, max: u64 }, + + #[error("Empty routes")] + EmptyRoutes {}, + + #[error("Pool {pool_id} doesn't have denom {denom}")] + InvalidPoolDenom { pool_id: u64, denom: String }, + + #[error("Message contains duplicated routes")] + DuplicatedRoutes {}, + + #[error("Route cannot start with ASTRO. Error in route: {route:?}")] + AstroInRoute { route: PoolRoute }, + + #[error("No registered route for {denom}")] + RouteNotFound { denom: String }, + + #[error("Collect cooldown has not elapsed. Next collect is possible at {next_collect_ts}")] + Cooldown { next_collect_ts: u64 }, + + #[error("Failed to build route for {denom}. Max swap depth {MAX_SWAPS_DEPTH}. Check for possible loops. Route taken: {route_taken}")] + FailedToBuildRoute { denom: String, route_taken: String }, + + #[error("Invalid reply id")] + InvalidReplyId {}, + + #[error("Empty collectable assets vector")] + EmptyAssets {}, + + #[error("Nothing to collect")] + NothingToCollect {}, +} diff --git a/contracts/maker/src/execute.rs b/contracts/maker/src/execute.rs new file mode 100644 index 0000000..c184d7f --- /dev/null +++ b/contracts/maker/src/execute.rs @@ -0,0 +1,487 @@ +use astroport::asset::validate_native_denom; +use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + attr, coin, ensure, Coin, Decimal, DepsMut, Env, MessageInfo, ReplyOn, Response, StdError, + SubMsg, +}; +use cw_utils::nonpayable; +use itertools::Itertools; +use osmosis_std::types::osmosis::poolmanager::v1beta1::{MsgSwapExactAmountIn, PoolmanagerQuerier}; + +use astroport_on_osmosis::maker::{CoinWithLimit, ExecuteMsg, PoolRoute, MAX_ALLOWED_SPREAD}; + +use crate::error::ContractError; +use crate::reply::POST_COLLECT_REPLY_ID; +use crate::state::{RouteStep, CONFIG, LAST_COLLECT_TS, OWNERSHIP_PROPOSAL, ROUTES}; +use crate::utils::{query_out_amount, validate_cooldown, RoutesBuilder}; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + // All maker endpoints are non-payable + nonpayable(&info)?; + + match msg { + ExecuteMsg::Collect { assets } => collect(deps, env, assets), + ExecuteMsg::UpdateConfig { + astro_denom, + fee_receiver, + max_spread, + collect_cooldown, + } => update_config( + deps, + info, + astro_denom, + fee_receiver, + max_spread, + collect_cooldown, + ), + ExecuteMsg::SetPoolRoutes(routes) => set_pool_routes(deps, info, routes), + ExecuteMsg::ProposeNewOwner { owner, expires_in } => { + let config = CONFIG.load(deps.storage)?; + propose_new_owner( + deps, + info, + env, + owner, + expires_in, + config.owner, + OWNERSHIP_PROPOSAL, + ) + .map_err(Into::into) + } + ExecuteMsg::DropOwnershipProposal {} => { + let config = CONFIG.load(deps.storage)?; + drop_ownership_proposal(deps, info, config.owner, OWNERSHIP_PROPOSAL) + .map_err(Into::into) + } + ExecuteMsg::ClaimOwnership {} => { + claim_ownership(deps, info, env, OWNERSHIP_PROPOSAL, |deps, new_owner| { + CONFIG + .update::<_, StdError>(deps.storage, |mut v| { + v.owner = new_owner; + Ok(v) + }) + .map(|_| ()) + }) + .map_err(Into::into) + } + } +} + +pub fn collect( + deps: DepsMut, + env: Env, + assets: Vec, +) -> Result { + ensure!(!assets.is_empty(), ContractError::EmptyAssets {}); + + let config = CONFIG.load(deps.storage)?; + + // Allowing collect only once per cooldown period + LAST_COLLECT_TS.update(deps.storage, |last_ts| match config.collect_cooldown { + Some(cd_period) if env.block.time.seconds() < last_ts + cd_period => { + Err(ContractError::Cooldown { + next_collect_ts: last_ts + cd_period, + }) + } + _ => Ok(env.block.time.seconds()), + })?; + + let mut messages = vec![]; + let mut attrs = vec![attr("action", "collect")]; + + let mut routes_builder = RoutesBuilder::default(); + for asset in assets { + let balance = deps + .querier + .query_balance(&env.contract.address, &asset.denom) + .map(|coin| { + asset + .amount + .map(|amount| Coin { + denom: coin.denom.to_string(), + amount: amount.min(coin.amount), + }) + .unwrap_or(coin) + })?; + + // Skip silently if the balance is zero. + // This allows our bot to operate normally without manual adjustments. + if balance.amount.is_zero() { + continue; + } + + attrs.push(attr("collected_asset", &balance.to_string())); + + let built_routes = + routes_builder.build_routes(deps.storage, &balance.denom, &config.astro_denom)?; + + attrs.push(attr("route_taken", built_routes.route_taken)); + + let out_amount = query_out_amount(deps.querier, &balance, &built_routes.routes)?; + + let min_out_amount = (Decimal::one() - config.max_spread) * out_amount; + + let swap_msg = MsgSwapExactAmountIn { + sender: env.contract.address.to_string(), + routes: built_routes.routes, + token_in: Some(coin(balance.amount.u128(), balance.denom.clone()).into()), + token_out_min_amount: min_out_amount.to_string(), + }; + messages.push(SubMsg::new(swap_msg)); + } + + messages + .last_mut() + .map(|submsg| { + submsg.id = POST_COLLECT_REPLY_ID; + submsg.reply_on = ReplyOn::Success; + }) + .ok_or(ContractError::NothingToCollect {})?; + + Ok(Response::new() + .add_submessages(messages) + .add_attributes(attrs)) +} + +pub fn update_config( + deps: DepsMut, + info: MessageInfo, + astro_denom: Option, + fee_receiver: Option, + max_spread: Option, + collect_cooldown: Option, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + ensure!(info.sender == config.owner, ContractError::Unauthorized {}); + + let mut attrs = vec![]; + + if let Some(astro_denom) = astro_denom { + validate_native_denom(&astro_denom)?; + attrs.push(attr("new_astro_denom", &astro_denom)); + config.astro_denom = astro_denom; + } + + if let Some(fee_receiver) = fee_receiver { + config.satellite = deps.api.addr_validate(&fee_receiver)?; + attrs.push(attr("new_fee_receiver", &fee_receiver)); + } + + if let Some(max_spread) = max_spread { + ensure!( + max_spread <= MAX_ALLOWED_SPREAD, + ContractError::MaxSpreadTooHigh {} + ); + attrs.push(attr("new_max_spread", max_spread.to_string())); + config.max_spread = max_spread; + } + + if let Some(collect_cooldown_val) = collect_cooldown { + validate_cooldown(collect_cooldown)?; + attrs.push(attr( + "new_collect_cooldown", + collect_cooldown_val.to_string(), + )); + config.collect_cooldown = Some(collect_cooldown_val); + } + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new().add_attributes(attrs)) +} + +pub fn set_pool_routes( + deps: DepsMut, + info: MessageInfo, + routes: Vec, +) -> Result { + ensure!(!routes.is_empty(), ContractError::EmptyRoutes {}); + ensure!( + routes.iter().map(|r| &r.denom_in).all_unique(), + ContractError::DuplicatedRoutes {} + ); + + let config = CONFIG.load(deps.storage)?; + ensure!(info.sender == config.owner, ContractError::Unauthorized {}); + + let mut attrs = vec![attr("action", "set_pool_routes")]; + + let mut routes_builder = RoutesBuilder::default(); + + for route in &routes { + ensure!( + route.denom_in != config.astro_denom, + ContractError::AstroInRoute { + route: route.clone() + } + ); + + // Sanity checks via osmosis pool manager + let pm_quierier = PoolmanagerQuerier::new(&deps.querier); + let pool_denoms = pm_quierier + .total_pool_liquidity(route.pool_id)? + .liquidity + .into_iter() + .map(|coin| coin.denom) + .collect_vec(); + + ensure!( + pool_denoms.contains(&route.denom_in), + ContractError::InvalidPoolDenom { + pool_id: route.pool_id, + denom: route.denom_in.to_owned() + } + ); + ensure!( + pool_denoms.contains(&route.denom_out), + ContractError::InvalidPoolDenom { + pool_id: route.pool_id, + denom: route.denom_out.to_owned() + } + ); + + if ROUTES.has(deps.storage, &route.denom_in) { + attrs.push(attr("updated_route", &route.denom_in)); + } + + let route_step = RouteStep { + denom_out: route.denom_out.to_owned(), + pool_id: route.pool_id, + }; + + // If route exists then this iteration updates the route. + ROUTES.save(deps.storage, &route.denom_in, &route_step)?; + + routes_builder + .routes_cache + .insert(route.denom_in.clone(), route_step); + } + + // Check all updated routes end up in ASTRO. It also checks for possible loops. + routes.iter().try_for_each(|route| { + routes_builder + .build_routes(deps.storage, &route.denom_in, &config.astro_denom) + .map(|_| ()) + })?; + + Ok(Response::new().add_attributes(attrs)) +} + +#[cfg(test)] +mod unit_tests { + use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; + use cosmwasm_std::{coins, Addr}; + use cw_utils::PaymentError; + + use astroport_on_osmosis::maker::{Config, COOLDOWN_LIMITS}; + + use super::*; + + #[test] + fn collect_basic_tests() { + let mut deps = mock_dependencies(); + + let assets = vec![]; + let err = collect(deps.as_mut(), mock_env(), assets).unwrap_err(); + assert_eq!(err, ContractError::EmptyAssets {}); + + let mut env = mock_env(); + let config = Config { + owner: Addr::unchecked("owner"), + astro_denom: "astro".to_string(), + satellite: Addr::unchecked("satellite"), + max_spread: Default::default(), + collect_cooldown: Some(60), + }; + CONFIG.save(deps.as_mut().storage, &config).unwrap(); + LAST_COLLECT_TS + .save(deps.as_mut().storage, &env.block.time.seconds()) + .unwrap(); + let assets = vec![CoinWithLimit { + denom: "uusd".to_string(), + amount: None, + }]; + let err = collect(deps.as_mut(), env.clone(), assets.clone()).unwrap_err(); + assert_eq!( + err, + ContractError::Cooldown { + next_collect_ts: env.block.time.seconds() + config.collect_cooldown.unwrap(), + } + ); + + env.block.time = env + .block + .time + .plus_seconds(config.collect_cooldown.unwrap()); + let err = collect(deps.as_mut(), env.clone(), assets).unwrap_err(); + assert_eq!(err, ContractError::NothingToCollect {}); + } + + #[test] + fn update_config_basic_tests() { + let mut deps = mock_dependencies(); + let config = Config { + owner: Addr::unchecked("owner"), + astro_denom: "astro".to_string(), + satellite: Addr::unchecked("satellite"), + max_spread: Default::default(), + collect_cooldown: Some(60), + }; + CONFIG.save(deps.as_mut().storage, &config).unwrap(); + + let err = execute( + deps.as_mut(), + mock_env(), + mock_info("random", &[]), + ExecuteMsg::UpdateConfig { + astro_denom: None, + fee_receiver: None, + max_spread: None, + collect_cooldown: None, + }, + ) + .unwrap_err(); + assert_eq!(err, ContractError::Unauthorized {}); + + let err = update_config( + deps.as_mut(), + mock_info(config.owner.as_str(), &[]), + Some("1a".to_string()), + None, + None, + None, + ) + .unwrap_err(); + assert_eq!( + err, + ContractError::Std(StdError::generic_err("Invalid denom length [3,128]: 1a")) + ); + + let err = update_config( + deps.as_mut(), + mock_info(config.owner.as_str(), &[]), + None, + Some("s".to_string()), + None, + None, + ) + .unwrap_err(); + assert_eq!( + err, + ContractError::Std(StdError::generic_err("Invalid input: human address too short for this mock implementation (must be >= 3).")) + ); + + let err = update_config( + deps.as_mut(), + mock_info(config.owner.as_str(), &[]), + None, + None, + Some(Decimal::percent(99)), + None, + ) + .unwrap_err(); + assert_eq!(err, ContractError::MaxSpreadTooHigh {}); + + let err = update_config( + deps.as_mut(), + mock_info(config.owner.as_str(), &[]), + None, + None, + None, + Some(COOLDOWN_LIMITS.end() + 1), + ) + .unwrap_err(); + assert_eq!(err, ContractError::IncorrectCooldown { min: 30, max: 600 }); + + update_config( + deps.as_mut(), + mock_info(config.owner.as_str(), &[]), + Some("new_astro".to_string()), + Some("new_fee_receiver".to_string()), + Some(Decimal::percent(10)), + Some(*COOLDOWN_LIMITS.start()), + ) + .unwrap(); + } + + #[test] + fn set_routes_basic_tests() { + let mut deps = mock_dependencies(); + let config = Config { + owner: Addr::unchecked("owner"), + astro_denom: "astro".to_string(), + satellite: Addr::unchecked("satellite"), + max_spread: Default::default(), + collect_cooldown: Some(60), + }; + CONFIG.save(deps.as_mut().storage, &config).unwrap(); + + let routes = vec![PoolRoute { + denom_in: "uatom".to_string(), + denom_out: "utest".to_string(), + pool_id: 1, + }]; + let err = + set_pool_routes(deps.as_mut(), mock_info("random", &[]), routes.clone()).unwrap_err(); + assert_eq!(err, ContractError::Unauthorized {}); + + let routes = vec![ + PoolRoute { + denom_in: "uatom".to_string(), + denom_out: "utest".to_string(), + pool_id: 1, + }, + PoolRoute { + denom_in: "uatom".to_string(), + denom_out: "ucoin".to_string(), + pool_id: 2, + }, + ]; + let err = set_pool_routes( + deps.as_mut(), + mock_info(config.owner.as_str(), &[]), + routes.clone(), + ) + .unwrap_err(); + assert_eq!(err, ContractError::DuplicatedRoutes {}); + + let wrong_route = PoolRoute { + denom_in: "astro".to_string(), + denom_out: "utest".to_string(), + pool_id: 1, + }; + let routes = vec![wrong_route.clone()]; + let err = set_pool_routes( + deps.as_mut(), + mock_info(config.owner.as_str(), &[]), + routes.clone(), + ) + .unwrap_err(); + assert_eq!(err, ContractError::AstroInRoute { route: wrong_route }); + } + + #[test] + fn test_nonpayable() { + let mut deps = mock_dependencies(); + + let res = execute( + deps.as_mut(), + mock_env(), + mock_info("test", &coins(1, "uosmo")), + ExecuteMsg::Collect { assets: vec![] }, + ) + .unwrap_err(); + assert_eq!( + res, + ContractError::PaymentError(PaymentError::NonPayable {}) + ); + } +} diff --git a/contracts/maker/src/instantiate.rs b/contracts/maker/src/instantiate.rs new file mode 100644 index 0000000..26c60b2 --- /dev/null +++ b/contracts/maker/src/instantiate.rs @@ -0,0 +1,47 @@ +use astroport::asset::validate_native_denom; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ensure, DepsMut, Env, MessageInfo, Response}; +use cw2::set_contract_version; + +use astroport_on_osmosis::maker::{Config, InstantiateMsg, MAX_ALLOWED_SPREAD}; + +use crate::error::ContractError; +use crate::state::{CONFIG, LAST_COLLECT_TS}; +use crate::utils::validate_cooldown; + +/// Contract name for cw2 info +pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +/// Contract version for cw2 info +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + validate_native_denom(&msg.astro_denom)?; + ensure!( + msg.max_spread <= MAX_ALLOWED_SPREAD, + ContractError::MaxSpreadTooHigh {} + ); + validate_cooldown(msg.collect_cooldown)?; + LAST_COLLECT_TS.save(deps.storage, &env.block.time.seconds())?; + + CONFIG.save( + deps.storage, + &Config { + owner: deps.api.addr_validate(&msg.owner)?, + astro_denom: msg.astro_denom, + satellite: deps.api.addr_validate(&msg.satellite)?, + max_spread: msg.max_spread, + collect_cooldown: None, + }, + )?; + + Ok(Response::new()) +} diff --git a/contracts/maker/src/lib.rs b/contracts/maker/src/lib.rs new file mode 100644 index 0000000..76236a6 --- /dev/null +++ b/contracts/maker/src/lib.rs @@ -0,0 +1,8 @@ +pub mod error; +pub mod execute; +pub mod instantiate; +pub mod migrate; +pub mod query; +pub mod reply; +pub mod state; +pub mod utils; diff --git a/contracts/maker/src/migrate.rs b/contracts/maker/src/migrate.rs new file mode 100644 index 0000000..98cd651 --- /dev/null +++ b/contracts/maker/src/migrate.rs @@ -0,0 +1,56 @@ +#![cfg(not(tarpaulin_include))] + +use astroport::asset::AssetInfo; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response}; +use cw_storage_plus::Map; + +use astroport_on_osmosis::maker::InstantiateMsg; + +use crate::error::ContractError; +use crate::instantiate::{instantiate, CONTRACT_NAME, CONTRACT_VERSION}; + +const EXPECTED_CONTRACT_NAME: &str = "astroport-maker"; +const EXPECTED_CONTRACT_VERSION: &str = "1.4.0"; + +/// This migration is used to convert the dummy maker contract on Osmosis into real working Maker. +/// +/// Mainnet contract which is only subject of this migration: https://celatone.osmosis.zone/osmosis-1/contracts/osmo1kl96qztvtrz9h8873jlcl9fz0fmgtdgfdw65shm9frr9y7xvc83qstjd8h +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, env: Env, msg: InstantiateMsg) -> Result { + cw2::assert_contract_version( + deps.storage, + EXPECTED_CONTRACT_NAME, + EXPECTED_CONTRACT_VERSION, + )?; + + // Clear old state + Map::::new("bridges").clear(deps.storage); + + let cw_admin = deps + .querier + .query_wasm_contract_info(&env.contract.address)? + .admin + .unwrap(); + // Even though info object is ignored in instantiate, we provide it for clarity + let info = MessageInfo { + sender: Addr::unchecked(cw_admin), + funds: vec![], + }; + // Instantiate state. + // Config and cw2 info will be overwritten. + let contract_version = cw2::get_contract_version(deps.storage)?; + + instantiate(deps, env, info, msg).map(|resp| { + resp.add_attributes([ + ("previous_contract_name", contract_version.contract.as_str()), + ( + "previous_contract_version", + contract_version.version.as_str(), + ), + ("new_contract_name", CONTRACT_NAME), + ("new_contract_version", CONTRACT_VERSION), + ]) + }) +} diff --git a/contracts/maker/src/query.rs b/contracts/maker/src/query.rs new file mode 100644 index 0000000..837854c --- /dev/null +++ b/contracts/maker/src/query.rs @@ -0,0 +1,85 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{to_json_binary, Binary, Coin, Deps, Env, Order, StdResult, Storage, Uint128}; +use cw_storage_plus::Bound; +use itertools::Itertools; + +use astroport_on_osmosis::maker::{ + PoolRoute, QueryMsg, SwapRouteResponse, DEFAULT_PAGINATION_LIMIT, +}; + +use crate::error::ContractError; +use crate::state::{CONFIG, ROUTES}; +use crate::utils::{query_out_amount, RoutesBuilder}; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { + let result = match msg { + QueryMsg::Config {} => to_json_binary(&CONFIG.load(deps.storage)?), + QueryMsg::Route { + denom_in, + denom_out, + } => to_json_binary(&query_route(deps.storage, &denom_in, &denom_out)?), + QueryMsg::Routes { start_after, limit } => { + to_json_binary(&query_routes(deps.storage, start_after, limit)?) + } + QueryMsg::EstimateExactInSwap { coin_in } => { + to_json_binary(&estimate_exact_swap_in(deps, coin_in)?) + } + }?; + + Ok(result) +} + +pub fn query_route( + storage: &dyn Storage, + denom_in: &str, + denom_out: &str, +) -> Result, ContractError> { + let routes = RoutesBuilder::default() + .build_routes(storage, denom_in, denom_out)? + .routes + .into_iter() + .map(|route| SwapRouteResponse { + pool_id: route.pool_id, + token_out_denom: route.token_out_denom.to_string(), + }) + .collect_vec(); + + Ok(routes) +} + +pub fn query_routes( + storage: &dyn Storage, + start_after: Option, + limit: Option, +) -> StdResult> { + let limit = limit.unwrap_or(DEFAULT_PAGINATION_LIMIT) as usize; + + ROUTES + .range( + storage, + start_after.as_deref().map(Bound::exclusive), + None, + Order::Ascending, + ) + .map(|item| { + item.map(|(denom_in, route_step)| PoolRoute { + denom_in, + denom_out: route_step.denom_out, + pool_id: route_step.pool_id, + }) + }) + .take(limit) + .collect() +} + +pub fn estimate_exact_swap_in(deps: Deps, coin_in: Coin) -> Result { + let config = CONFIG.load(deps.storage)?; + + let mut routes_builder = RoutesBuilder::default(); + let built_routes = + routes_builder.build_routes(deps.storage, &coin_in.denom, &config.astro_denom)?; + + query_out_amount(deps.querier, &coin_in, &built_routes.routes) +} diff --git a/contracts/maker/src/reply.rs b/contracts/maker/src/reply.rs new file mode 100644 index 0000000..2dd30f6 --- /dev/null +++ b/contracts/maker/src/reply.rs @@ -0,0 +1,38 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{attr, wasm_execute, DepsMut, Empty, Env, Reply, Response, SubMsg}; + +use crate::error::ContractError; +use crate::state::CONFIG; + +pub const POST_COLLECT_REPLY_ID: u64 = 1; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result { + match msg.id { + POST_COLLECT_REPLY_ID => { + let config = CONFIG.load(deps.storage)?; + let astro_balance = deps + .querier + .query_balance(env.contract.address, &config.astro_denom)?; + + let mut response = Response::new().add_attributes([ + attr("action", "post_collect_reply"), + attr("astro", astro_balance.to_string()), + ]); + + let transfer_msg = wasm_execute( + config.satellite, + // Satellite type parameter is only needed for CheckMessages endpoint which is not used in Maker contract. + // So it's safe to pass Empty as CustomMsg + &astro_satellite_package::ExecuteMsg::::TransferAstro {}, + vec![astro_balance], + )?; + + response.messages.push(SubMsg::new(transfer_msg)); + + Ok(response) + } + _ => Err(ContractError::InvalidReplyId {}), + } +} diff --git a/contracts/maker/src/state.rs b/contracts/maker/src/state.rs new file mode 100644 index 0000000..ecc6011 --- /dev/null +++ b/contracts/maker/src/state.rs @@ -0,0 +1,21 @@ +use astroport::common::OwnershipProposal; +use cosmwasm_schema::cw_serde; +use cw_storage_plus::{Item, Map}; + +use astroport_on_osmosis::maker::Config; + +/// Routes is a map of denom_in and denom_out to pool_id. +/// Key: (denom_in), Value: RouteStep object {denom_out, pool_id} +pub const ROUTES: Map<&str, RouteStep> = Map::new("routes"); +/// Config is the general settings of the contract. +pub const CONFIG: Item = Item::new("config"); +/// Stores the latest timestamp when fees were collected +pub const LAST_COLLECT_TS: Item = Item::new("last_collect_ts"); +/// Stores the latest proposal to change contract ownership +pub const OWNERSHIP_PROPOSAL: Item = Item::new("ownership_proposal"); + +#[cw_serde] +pub struct RouteStep { + pub denom_out: String, + pub pool_id: u64, +} diff --git a/contracts/maker/src/utils.rs b/contracts/maker/src/utils.rs new file mode 100644 index 0000000..079f66a --- /dev/null +++ b/contracts/maker/src/utils.rs @@ -0,0 +1,148 @@ +use std::collections::HashMap; +use std::str::FromStr; + +use cosmwasm_std::{ + ensure, Coin, Decimal, Fraction, QuerierWrapper, StdError, StdResult, Storage, Uint128, +}; +use itertools::Itertools; +use osmosis_std::types::osmosis::poolmanager::v1beta1::{PoolmanagerQuerier, SwapAmountInRoute}; + +use astroport_on_osmosis::maker::{COOLDOWN_LIMITS, MAX_SWAPS_DEPTH}; + +use crate::error::ContractError; +use crate::state::{RouteStep, ROUTES}; + +/// Validate cooldown value is within the allowed range +pub fn validate_cooldown(maybe_cooldown: Option) -> Result<(), ContractError> { + if let Some(collect_cooldown) = maybe_cooldown { + if !COOLDOWN_LIMITS.contains(&collect_cooldown) { + return Err(ContractError::IncorrectCooldown { + min: *COOLDOWN_LIMITS.start(), + max: *COOLDOWN_LIMITS.end(), + }); + } + } + + Ok(()) +} + +/// Query how much amount of denom_out we get for denom_in. +/// Copied from Mars: https://github.com/mars-protocol/contracts/blob/28edbfb37768cc6c73b854ce5d95b2655951af58/contracts/swapper/osmosis/src/route.rs#L193 +/// +/// Example calculation: +/// If we want to swap atom to usdc and configured routes are [pool_1 (atom/osmo), pool_69 (osmo/usdc)] (no direct pool of atom/usdc): +/// 1) query pool_1 to get price for atom/osmo +/// 2) query pool_69 to get price for osmo/usdc +/// 3) atom/usdc = (price for atom/osmo) * (price for osmo/usdc) +/// 4) usdc_out_amount = (atom amount) * (price for atom/usdc) +pub fn query_out_amount( + querier: QuerierWrapper, + coin_in: &Coin, + steps: &[SwapAmountInRoute], +) -> Result { + let mut price = Decimal::one(); + let mut denom_in = coin_in.denom.clone(); + for step in steps { + let step_price = query_spot_price(querier, step.pool_id, &denom_in, &step.token_out_denom)?; + price = price.checked_mul(step_price)?; + denom_in = step.token_out_denom.clone(); + } + + let out_amount = coin_in + .amount + .checked_multiply_ratio(price.numerator(), price.denominator())?; + Ok(out_amount) +} + +/// Query spot price of a coin, denominated in quote_denom. +pub fn query_spot_price( + querier: QuerierWrapper, + pool_id: u64, + base_denom: &str, + quote_denom: &str, +) -> StdResult { + let spot_price_res = PoolmanagerQuerier::new(&querier).spot_price( + pool_id, + base_denom.to_string(), + quote_denom.to_string(), + )?; + let price = Decimal::from_str(&spot_price_res.spot_price)?; + if price.is_zero() { + Err(StdError::generic_err(format!( + "Zero spot price. pool_id {pool_id} base_denom {base_denom} quote_denom {quote_denom}", + ))) + } else { + Ok(price) + } +} + +#[derive(Default)] +pub struct RoutesBuilder { + pub routes_cache: HashMap, +} + +pub struct BuiltRoutes { + pub routes: Vec, + pub route_taken: String, +} + +impl RoutesBuilder { + pub fn build_routes( + &mut self, + storage: &dyn Storage, + denom_in: &str, + astro_denom: &str, + ) -> Result { + let mut prev_denom = denom_in.to_string(); + let mut routes = vec![]; + + for _ in 0..MAX_SWAPS_DEPTH { + if prev_denom == astro_denom { + break; + } + + let step = if let Some(found) = self.routes_cache.get(&prev_denom).cloned() { + found + } else { + let step = + ROUTES + .may_load(storage, &prev_denom)? + .ok_or(ContractError::RouteNotFound { + denom: prev_denom.to_string(), + })?; + self.routes_cache + .insert(prev_denom.to_string(), step.clone()); + + step + }; + + routes.push(SwapAmountInRoute { + pool_id: step.pool_id, + token_out_denom: step.denom_out.clone(), + }); + + prev_denom = step.denom_out; + } + + let route_denoms = routes + .iter() + .map(|r| r.token_out_denom.clone()) + .collect_vec(); + let route_taken = [vec![denom_in.to_string()], route_denoms] + .concat() + .join(" -> "); + + ensure!( + prev_denom == astro_denom, + ContractError::FailedToBuildRoute { + denom: denom_in.to_string(), + route_taken, + } + ); + + Ok(BuiltRoutes { + routes, + route_taken, + }) + } +} diff --git a/contracts/maker/tests/common/helper.rs b/contracts/maker/tests/common/helper.rs new file mode 100644 index 0000000..2c6aada --- /dev/null +++ b/contracts/maker/tests/common/helper.rs @@ -0,0 +1,460 @@ +#![allow(dead_code)] + +use std::error::Error; +use std::fmt::Display; +use std::str::FromStr; + +use anyhow::Result as AnyResult; +use astroport::asset::{Asset, AssetInfo, PairInfo}; +use astroport::factory::{PairConfig, PairType}; +use astroport::pair_concentrated::ConcentratedPoolParams; +use astroport::{factory, pair}; +use cosmwasm_std::testing::MockApi; +use cosmwasm_std::{ + coins, to_json_binary, Addr, Api, Binary, Coin, Decimal, Deps, DepsMut, Empty, Env, GovMsg, + IbcMsg, IbcQuery, MemoryStorage, MessageInfo, Response, StdResult, Storage, +}; +use cw_multi_test::{ + AddressGenerator, App, AppResponse, BankKeeper, BankSudo, BasicAppBuilder, Contract, + ContractWrapper, DistributionKeeper, Executor, FailingModule, StakeKeeper, WasmKeeper, +}; +use cw_storage_plus::Item; +use derivative::Derivative; +use itertools::Itertools; + +use astroport_on_osmosis::maker; +use astroport_on_osmosis::maker::{CoinWithLimit, PoolRoute, SwapRouteResponse}; +use astroport_on_osmosis::pair_pcl::ExecuteMsg; +use astroport_pcl_osmo::contract::{execute, instantiate, reply}; +use astroport_pcl_osmo::queries::query; +use astroport_pcl_osmo::state::POOL_ID; +use astroport_pcl_osmo::sudo::sudo; + +use crate::common::osmosis_ext::OsmosisStargate; + +pub type OsmoApp = App< + BankKeeper, + MockApi, + MemoryStorage, + FailingModule, + WasmKeeper, + StakeKeeper, + DistributionKeeper, + FailingModule, + FailingModule, + OsmosisStargate, +>; + +fn pair_contract() -> Box> { + Box::new( + ContractWrapper::new_with_empty(execute, instantiate, query) + .with_reply_empty(reply) + .with_sudo_empty(sudo), + ) +} + +fn coin_registry_contract() -> Box> { + Box::new(ContractWrapper::new_with_empty( + astroport_native_coin_registry::contract::execute, + astroport_native_coin_registry::contract::instantiate, + astroport_native_coin_registry::contract::query, + )) +} +fn factory_contract() -> Box> { + Box::new( + ContractWrapper::new_with_empty( + astroport_factory::contract::execute, + astroport_factory::contract::instantiate, + astroport_factory::contract::query, + ) + .with_reply_empty(astroport_factory::contract::reply), + ) +} + +fn maker_contract() -> Box> { + Box::new( + ContractWrapper::new_with_empty( + astroport_maker_osmosis::execute::execute, + astroport_maker_osmosis::instantiate::instantiate, + astroport_maker_osmosis::query::query, + ) + .with_reply_empty(astroport_maker_osmosis::reply::reply), + ) +} + +fn mock_satellite_contract() -> Box> { + let instantiate = |_: DepsMut, _: Env, _: MessageInfo, _: Empty| -> StdResult { + Ok(Default::default()) + }; + let execute = |_: DepsMut, + _: Env, + _: MessageInfo, + _: astro_satellite_package::ExecuteMsg| + -> StdResult { Ok(Default::default()) }; + let empty_query = |_: Deps, _: Env, _: Empty| -> StdResult { unimplemented!() }; + + Box::new(ContractWrapper::new_with_empty( + execute, + instantiate, + empty_query, + )) +} + +pub fn osmo_create_pair_fee() -> Vec { + coins(1000_000000, "uosmo") +} + +fn common_pcl_params(price_scale: Decimal) -> ConcentratedPoolParams { + ConcentratedPoolParams { + amp: f64_to_dec(10f64), + gamma: f64_to_dec(0.000145), + mid_fee: f64_to_dec(0.0026), + out_fee: f64_to_dec(0.0045), + fee_gamma: f64_to_dec(0.00023), + repeg_profit_threshold: f64_to_dec(0.000002), + min_price_scale_delta: f64_to_dec(0.000146), + price_scale, + ma_half_time: 600, + track_asset_balances: None, + fee_share: None, + } +} + +const FACTORY_ADDRESS: &str = include_str!("../../../pair_concentrated/src/factory_address"); + +#[derive(Default)] +struct HackyAddressGenerator<'a> { + _phantom: std::marker::PhantomData<&'a ()>, +} + +impl<'a> HackyAddressGenerator<'a> { + pub const FACTORY_MARKER: Item<'a, ()> = Item::new("factory_marker"); +} + +impl<'a> AddressGenerator for HackyAddressGenerator<'a> { + fn contract_address( + &self, + _api: &dyn Api, + storage: &mut dyn Storage, + _code_id: u64, + instance_id: u64, + ) -> AnyResult { + if Self::FACTORY_MARKER.may_load(storage).unwrap().is_some() { + Self::FACTORY_MARKER.remove(storage); + Ok(Addr::unchecked(FACTORY_ADDRESS)) + } else { + Ok(Addr::unchecked(format!("contract{instance_id}"))) + } + } +} + +pub const ASTRO_DENOM: &str = "astro"; + +#[derive(Derivative)] +#[derivative(Debug)] +pub struct Helper { + #[derivative(Debug = "ignore")] + pub app: OsmoApp, + pub owner: Addr, + pub coin_registry: Addr, + pub factory: Addr, + pub maker: Addr, + pub satellite: Addr, +} + +impl Helper { + pub fn new(owner: &Addr) -> AnyResult { + let wasm_keeper = + WasmKeeper::new().with_address_generator(HackyAddressGenerator::default()); + let mut app = BasicAppBuilder::new() + .with_stargate(OsmosisStargate::default()) + .with_wasm(wasm_keeper) + .build(|router, _, storage| { + router + .bank + .init_balance(storage, owner, coins(1_000_000_000_000, "uosmo")) + .unwrap() + }); + + let pair_code_id = app.store_code(pair_contract()); + let factory_code_id = app.store_code(factory_contract()); + let satellite_code_id = app.store_code(mock_satellite_contract()); + + let satellite = app.instantiate_contract( + satellite_code_id, + owner.clone(), + &Empty {}, + &[], + "Satellite", + None, + )?; + + let maker_code_id = app.store_code(maker_contract()); + let maker = app.instantiate_contract( + maker_code_id, + owner.clone(), + &maker::InstantiateMsg { + owner: owner.to_string(), + astro_denom: ASTRO_DENOM.to_string(), + satellite: satellite.to_string(), + max_spread: Decimal::percent(10), + collect_cooldown: None, + }, + &[], + "Maker", + None, + )?; + + let coin_registry_id = app.store_code(coin_registry_contract()); + + let coin_registry = app + .instantiate_contract( + coin_registry_id, + owner.clone(), + &astroport::native_coin_registry::InstantiateMsg { + owner: owner.to_string(), + }, + &[], + "Coin registry", + None, + ) + .unwrap(); + + let init_msg = factory::InstantiateMsg { + fee_address: Some(maker.to_string()), + pair_configs: vec![PairConfig { + code_id: pair_code_id, + maker_fee_bps: 2500, + total_fee_bps: 0u16, // Concentrated pair does not use this field, + pair_type: PairType::Custom("concentrated".to_string()), + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }], + token_code_id: 0, + generator_address: None, + owner: owner.to_string(), + whitelist_code_id: 0, + coin_registry_address: coin_registry.to_string(), + }; + + // Set marker in storage that the next contract is factory. We need this to have exact FACTORY_ADDRESS constant + // which is hardcoded in the PCL code. + app.init_modules(|_, _, storage| HackyAddressGenerator::FACTORY_MARKER.save(storage, &())) + .unwrap(); + let factory = app.instantiate_contract( + factory_code_id, + owner.clone(), + &init_msg, + &[], + "Factory", + None, + )?; + + Ok(Self { + app, + owner: owner.clone(), + coin_registry, + factory, + maker, + satellite, + }) + } + + pub fn create_and_seed_pair( + &mut self, + initial_liquidity: [Coin; 2], + ) -> AnyResult<(PairInfo, u64)> { + let native_coins = initial_liquidity + .iter() + .cloned() + .map(|x| (x.denom.clone(), 6)) + .collect::>(); + let asset_infos = native_coins + .iter() + .map(|(denom, _)| AssetInfo::native(denom)) + .collect_vec(); + + self.app + .execute_contract( + self.owner.clone(), + self.coin_registry.clone(), + &astroport::native_coin_registry::ExecuteMsg::Add { native_coins }, + &[], + ) + .unwrap(); + + let price_scale = + Decimal::from_ratio(initial_liquidity[0].amount, initial_liquidity[1].amount); + let owner = self.owner.clone(); + + let pair_info = self + .app + .execute_contract( + owner.clone(), + self.factory.clone(), + &factory::ExecuteMsg::CreatePair { + pair_type: PairType::Custom("concentrated".to_string()), + asset_infos: asset_infos.clone(), + init_params: Some(to_json_binary(&common_pcl_params(price_scale)).unwrap()), + }, + &osmo_create_pair_fee(), + ) + .map(|_| self.query_pair_info(&asset_infos))?; + + let provide_assets = [ + Asset::native(&initial_liquidity[0].denom, initial_liquidity[0].amount), + Asset::native(&initial_liquidity[1].denom, initial_liquidity[1].amount), + ]; + + self.give_me_money(&provide_assets, &owner); + self.provide(&pair_info.contract_addr, &owner, &provide_assets) + .unwrap(); + + let pool_id = POOL_ID + .query(&self.app.wrap(), pair_info.contract_addr.clone()) + .unwrap(); + + Ok((pair_info, pool_id)) + } + + pub fn set_pool_routes(&mut self, pool_routes: Vec) -> AnyResult { + self.app.execute_contract( + self.owner.clone(), + self.maker.clone(), + &maker::ExecuteMsg::SetPoolRoutes(pool_routes), + &[], + ) + } + + pub fn collect(&mut self, assets: Vec) -> AnyResult { + self.app.execute_contract( + self.owner.clone(), + self.maker.clone(), + &maker::ExecuteMsg::Collect { assets }, + &[], + ) + } + + pub fn query_route(&self, denom_in: &str, denom_out: &str) -> Vec { + self.app + .wrap() + .query_wasm_smart( + &self.maker, + &maker::QueryMsg::Route { + denom_in: denom_in.to_string(), + denom_out: denom_out.to_string(), + }, + ) + .unwrap() + } + + pub fn query_pair_info(&self, asset_infos: &[AssetInfo]) -> PairInfo { + self.app + .wrap() + .query_wasm_smart( + &self.factory, + &factory::QueryMsg::Pair { + asset_infos: asset_infos.to_vec(), + }, + ) + .unwrap() + } + + pub fn provide( + &mut self, + pair: &Addr, + sender: &Addr, + assets: &[Asset], + ) -> AnyResult { + let funds = assets + .iter() + .map(|x| x.as_coin().unwrap()) + .collect::>(); + + let msg = ExecuteMsg::ProvideLiquidity { + assets: assets.to_vec(), + slippage_tolerance: Some(f64_to_dec(0.5)), + auto_stake: None, + receiver: None, + }; + + self.app + .execute_contract(sender.clone(), pair.clone(), &msg, &funds) + } + + pub fn swap( + &mut self, + pair: &Addr, + sender: &Addr, + offer_asset: &Asset, + max_spread: Option, + ) -> AnyResult { + match &offer_asset.info { + AssetInfo::NativeToken { .. } => self.app.execute_contract( + sender.clone(), + pair.clone(), + &pair::ExecuteMsg::Swap { + offer_asset: offer_asset.clone(), + ask_asset_info: None, + belief_price: None, + max_spread, + to: None, + }, + &[offer_asset.as_coin().unwrap()], + ), + AssetInfo::Token { .. } => unimplemented!("cw20 not implemented"), + } + } + + pub fn native_balance(&self, denom: &str, user: &Addr) -> u128 { + self.app + .wrap() + .query_balance(user, denom) + .unwrap() + .amount + .u128() + } + + pub fn give_me_money(&mut self, assets: &[Asset], recipient: &Addr) { + let funds = assets + .iter() + .map(|x| x.as_coin().unwrap()) + .collect::>(); + + self.app + .sudo( + BankSudo::Mint { + to_address: recipient.to_string(), + amount: funds, + } + .into(), + ) + .unwrap(); + } +} + +pub trait AppExtension { + fn next_block(&mut self, time: u64); +} + +impl AppExtension for OsmoApp { + fn next_block(&mut self, time: u64) { + self.update_block(|block| { + block.time = block.time.plus_seconds(time); + block.height += 1 + }); + } +} + +pub fn f64_to_dec(val: f64) -> T +where + T: FromStr, + T::Err: Error, +{ + T::from_str(&val.to_string()).unwrap() +} + +pub fn dec_to_f64(val: impl Display) -> f64 { + f64::from_str(&val.to_string()).unwrap() +} diff --git a/contracts/maker/tests/common/mod.rs b/contracts/maker/tests/common/mod.rs new file mode 100644 index 0000000..243040d --- /dev/null +++ b/contracts/maker/tests/common/mod.rs @@ -0,0 +1,2 @@ +pub mod helper; +pub mod osmosis_ext; diff --git a/contracts/maker/tests/common/osmosis_ext.rs b/contracts/maker/tests/common/osmosis_ext.rs new file mode 100644 index 0000000..033b010 --- /dev/null +++ b/contracts/maker/tests/common/osmosis_ext.rs @@ -0,0 +1,346 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::fmt::Debug; + +use anyhow::Result as AnyResult; +use astroport::pair::PoolResponse; +use cosmwasm_schema::schemars::JsonSchema; +use cosmwasm_schema::serde::de::DeserializeOwned; +use cosmwasm_std::{ + coin, coins, from_json, to_json_binary, Addr, Api, BankMsg, Binary, BlockInfo, CustomQuery, + Empty, Querier, QuerierWrapper, QueryRequest, Storage, SubMsgResponse, Uint128, WasmMsg, + WasmQuery, +}; +use cw_multi_test::{AppResponse, BankSudo, CosmosRouter, Stargate, WasmSudo}; +use osmosis_std::types::osmosis::cosmwasmpool::v1beta1::{ + ContractInfoByPoolIdRequest, ContractInfoByPoolIdResponse, MsgCreateCosmWasmPool, + MsgCreateCosmWasmPoolResponse, +}; +use osmosis_std::types::osmosis::poolmanager; +use osmosis_std::types::osmosis::poolmanager::v1beta1::{ + MsgSwapExactAmountIn, MsgSwapExactAmountOut, SpotPriceRequest, SpotPriceResponse, + TotalPoolLiquidityRequest, +}; +use osmosis_std::types::osmosis::tokenfactory::v1beta1::{ + MsgBurn, MsgCreateDenom, MsgCreateDenomResponse, MsgMint, +}; + +use astroport_on_osmosis::pair_pcl; +use astroport_on_osmosis::pair_pcl::{ + GetSwapFeeResponse, QueryMsg, SwapExactAmountInResponseData, SwapExactAmountOutResponseData, +}; + +#[derive(Default)] +pub struct OsmosisStargate { + pub cw_pools: RefCell>, +} + +impl Stargate for OsmosisStargate { + fn execute( + &self, + api: &dyn Api, + storage: &mut dyn Storage, + router: &dyn CosmosRouter, + block: &BlockInfo, + sender: Addr, + type_url: String, + value: Binary, + ) -> AnyResult + where + ExecC: Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static, + QueryC: CustomQuery + DeserializeOwned + 'static, + { + match type_url.as_str() { + MsgCreateCosmWasmPool::TYPE_URL => { + let cw_msg: MsgCreateCosmWasmPool = value.try_into()?; + let init_wasm = WasmMsg::Instantiate { + admin: None, + code_id: cw_msg.code_id, + msg: cw_msg.instantiate_msg.into(), + funds: vec![], + label: "CW pool: Astroport PCL".to_string(), + }; + let resp = router.execute(api, storage, block, sender, init_wasm.into())?; + let contract_addr = resp + .events + .iter() + .find_map(|e| { + if e.ty == "instantiate" { + Some( + e.attributes + .iter() + .find(|a| a.key == "_contract_address") + .unwrap() + .value + .clone(), + ) + } else { + None + } + }) + .unwrap(); + + let mut cw_pools = self.cw_pools.borrow_mut(); + let next_pool_id = cw_pools.len() as u64 + 1; + cw_pools.insert(next_pool_id, contract_addr); + + let submsg_response = SubMsgResponse { + events: vec![], + data: Some( + MsgCreateCosmWasmPoolResponse { + pool_id: next_pool_id, + } + .into(), + ), + }; + Ok(submsg_response.into()) + } + MsgCreateDenom::TYPE_URL => { + let tf_msg: MsgCreateDenom = value.try_into()?; + let submsg_response = SubMsgResponse { + events: vec![], + data: Some( + MsgCreateDenomResponse { + new_token_denom: format!( + "factory/{}/{}", + tf_msg.sender, tf_msg.subdenom + ), + } + .into(), + ), + }; + Ok(submsg_response.into()) + } + MsgMint::TYPE_URL => { + let tf_msg: MsgMint = value.try_into()?; + let mint_coins = tf_msg + .amount + .expect("Empty amount in tokenfactory MsgMint!"); + let bank_sudo = BankSudo::Mint { + to_address: tf_msg.mint_to_address, + amount: coins(mint_coins.amount.parse()?, mint_coins.denom), + }; + router.sudo(api, storage, block, bank_sudo.into()) + } + MsgBurn::TYPE_URL => { + let tf_msg: MsgBurn = value.try_into()?; + let burn_coins = tf_msg + .amount + .expect("Empty amount in tokenfactory MsgBurn!"); + let burn_msg = BankMsg::Burn { + amount: coins(burn_coins.amount.parse()?, burn_coins.denom), + }; + router.execute( + api, + storage, + block, + Addr::unchecked(tf_msg.sender), + burn_msg.into(), + ) + } + MsgSwapExactAmountIn::TYPE_URL => { + let pm_msg: MsgSwapExactAmountIn = value.try_into()?; + let token_in_data = pm_msg.token_in.expect("token_in must be set!"); + let mut token_in = coin(token_in_data.amount.parse()?, token_in_data.denom); + + let app_responses = pm_msg + .routes + .into_iter() + .map(|route| { + let contract_addr = + Addr::unchecked(&self.cw_pools.borrow()[&route.pool_id]); + + // Osmosis always performs this query before calling a contract. + let res = router + .query( + api, + storage, + block, + QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: contract_addr.to_string(), + msg: to_json_binary(&QueryMsg::GetSwapFee {}).unwrap(), + }), + ) + .unwrap(); + let swap_fee = from_json::(&res)?.swap_fee; + + // Send funds from sender to contract + router.execute( + api, + storage, + block, + Addr::unchecked(&pm_msg.sender), + BankMsg::Send { + to_address: contract_addr.to_string(), + amount: vec![token_in.clone()], + } + .into(), + )?; + + let inner_contract_msg = pair_pcl::SudoMessage::SwapExactAmountIn { + sender: pm_msg.sender.to_string(), + token_in: token_in.clone(), + token_out_denom: route.token_out_denom.clone(), + token_out_min_amount: pm_msg.token_out_min_amount.parse()?, + swap_fee, + }; + + let wasm_sudo_msg = WasmSudo::new(&contract_addr, &inner_contract_msg)?; + let res = router.sudo(api, storage, block, wasm_sudo_msg.into())?; + + let res_data: SwapExactAmountInResponseData = + from_json(res.data.as_ref().unwrap())?; + token_in = coin(res_data.token_out_amount.u128(), route.token_out_denom); + + Ok(res) + }) + .collect::>>()?; + + Ok(app_responses.last().cloned().unwrap()) + } + MsgSwapExactAmountOut::TYPE_URL => { + let pm_msg: MsgSwapExactAmountOut = value.try_into()?; + let token_out = pm_msg.token_out.expect("token_out must be set!"); + + let contract_addr = + Addr::unchecked(&self.cw_pools.borrow()[&pm_msg.routes[0].pool_id]); + + // Osmosis always performs this query before calling a contract. + let res = router + .query( + api, + storage, + block, + QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: contract_addr.to_string(), + msg: to_json_binary(&QueryMsg::GetSwapFee {}).unwrap(), + }), + ) + .unwrap(); + + let token_in_denom = pm_msg.routes[0].token_in_denom.clone(); + let token_in_max_amount: Uint128 = pm_msg.token_in_max_amount.parse()?; + + let inner_contract_msg = pair_pcl::SudoMessage::SwapExactAmountOut { + sender: pm_msg.sender.clone(), + token_in_denom: token_in_denom.clone(), + token_in_max_amount, + token_out: coin(token_out.amount.parse()?, token_out.denom), + swap_fee: from_json::(&res)?.swap_fee, + }; + + router.execute( + api, + storage, + block, + Addr::unchecked(&pm_msg.sender), + BankMsg::Send { + to_address: contract_addr.to_string(), + amount: coins( + pm_msg.token_in_max_amount.parse()?, + pm_msg.routes[0].token_in_denom.clone(), + ), + } + .into(), + )?; + + let wasm_sudo_msg = WasmSudo::new(&contract_addr, &inner_contract_msg)?; + let resp = router.sudo(api, storage, block, wasm_sudo_msg.into()); + + // Cosmwasmpool derives excess tokens itself and sends them back to the sender. + // https://github.com/osmosis-labs/osmosis/blob/294302637a47ffec5cafc0c1953e88a54390b20e/x/cosmwasmpool/pool_module.go#L316-L321 + // Mimic this logic here. + if let Ok(resp) = &resp { + let raw = resp.data.clone().expect("Data must be set in response"); + let token_in_amount = from_json::(&raw) + .unwrap() + .token_in_amount; + let excess_tokens = token_in_max_amount - token_in_amount; + + if !excess_tokens.is_zero() { + router.execute( + api, + storage, + block, + Addr::unchecked(contract_addr), + BankMsg::Send { + to_address: pm_msg.sender.to_string(), + amount: coins(excess_tokens.u128(), token_in_denom), + } + .into(), + )?; + } + } + + resp + } + _ => Err(anyhow::anyhow!( + "Unexpected exec msg {type_url} from {sender:?}", + )), + } + } + + fn query( + &self, + _api: &dyn Api, + _storage: &dyn Storage, + querier: &dyn Querier, + _block: &BlockInfo, + path: String, + data: Binary, + ) -> AnyResult { + match path.as_str() { + "/osmosis.cosmwasmpool.v1beta1.Query/ContractInfoByPoolId" => { + let inner: ContractInfoByPoolIdRequest = data.try_into()?; + let contract_address = self.cw_pools.borrow()[&inner.pool_id].clone(); + Ok(to_json_binary(&ContractInfoByPoolIdResponse { + contract_address, + code_id: 0, + })?) + } + "/osmosis.poolmanager.v1beta1.Query/Params" => { + Ok(to_json_binary(&poolmanager::v1beta1::ParamsResponse { + params: Some(poolmanager::v1beta1::Params { + pool_creation_fee: vec![coin(1000_000000, "uosmo").into()], + taker_fee_params: None, + authorized_quote_denoms: vec![], + }), + })?) + } + "/osmosis.poolmanager.v1beta1.Query/TotalPoolLiquidity" => { + let inner: TotalPoolLiquidityRequest = data.try_into()?; + let contract_address = self.cw_pools.borrow()[&inner.pool_id].clone(); + let liquidity = QuerierWrapper::::new(querier) + .query_wasm_smart::(&contract_address, &QueryMsg::Pool {}) + .map(|resp| { + resp.assets + .into_iter() + .map(|c| c.as_coin().unwrap().into()) + .collect() + })?; + + Ok(to_json_binary( + &poolmanager::v1beta1::TotalPoolLiquidityResponse { liquidity }, + )?) + } + "/osmosis.poolmanager.v1beta1.Query/SpotPrice" => { + let inner: SpotPriceRequest = data.try_into()?; + + let contract_address = self.cw_pools.borrow()[&inner.pool_id].clone(); + let querier = QuerierWrapper::::new(querier); + let spot_price: SpotPriceResponse = querier.query_wasm_smart( + &contract_address, + &QueryMsg::SpotPrice { + quote_asset_denom: inner.quote_asset_denom.to_string(), + base_asset_denom: inner.quote_asset_denom.to_string(), + }, + )?; + + Ok(to_json_binary(&SpotPriceResponse { + spot_price: spot_price.spot_price, + })?) + } + _ => Err(anyhow::anyhow!("Unexpected stargate query request {path}",)), + } + } +} diff --git a/contracts/maker/tests/maker_integration.rs b/contracts/maker/tests/maker_integration.rs new file mode 100644 index 0000000..8887500 --- /dev/null +++ b/contracts/maker/tests/maker_integration.rs @@ -0,0 +1,567 @@ +use astroport::asset::Asset; +use astroport::pair::ExecuteMsg; +use cosmwasm_std::{coin, Addr, Uint128}; +use cw_multi_test::Executor; +use itertools::Itertools; + +use astroport_maker_osmosis::error::ContractError; +use astroport_on_osmosis::maker::{CoinWithLimit, PoolRoute, SwapRouteResponse, MAX_SWAPS_DEPTH}; + +use crate::common::helper::{Helper, ASTRO_DENOM}; + +mod common; +#[test] +fn check_set_routes() { + let owner = Addr::unchecked("owner"); + let mut helper = Helper::new(&owner).unwrap(); + + let (_, astro_pool_id) = helper + .create_and_seed_pair([ + coin(1_000_000_000000, "uusd"), + coin(1_000_000_000000, ASTRO_DENOM), + ]) + .unwrap(); + + let (_, pool_1) = helper + .create_and_seed_pair([ + coin(1_000_000_000000, "ucoin"), + coin(1_000_000_000000, "uusd"), + ]) + .unwrap(); + + // Set wrong pool id + let err = helper + .set_pool_routes(vec![PoolRoute { + denom_in: "ucoin".to_string(), + denom_out: "uusd".to_string(), + pool_id: astro_pool_id, + }]) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::InvalidPoolDenom { + pool_id: astro_pool_id, + denom: "ucoin".to_string() + } + ); + let err = helper + .set_pool_routes(vec![PoolRoute { + denom_in: "ucoin".to_string(), + denom_out: "rand".to_string(), + pool_id: pool_1, + }]) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::InvalidPoolDenom { + pool_id: pool_1, + denom: "rand".to_string() + } + ); + + // ucoin -> uusd -> astro + helper + .set_pool_routes(vec![ + PoolRoute { + denom_in: "ucoin".to_string(), + denom_out: "uusd".to_string(), + pool_id: pool_1, + }, + PoolRoute { + denom_in: "uusd".to_string(), + denom_out: ASTRO_DENOM.to_string(), + pool_id: astro_pool_id, + }, + ]) + .unwrap(); + + let route = helper.query_route("ucoin", ASTRO_DENOM); + assert_eq!( + route, + vec![ + SwapRouteResponse { + pool_id: pool_1, + token_out_denom: "uusd".to_string(), + }, + SwapRouteResponse { + token_out_denom: ASTRO_DENOM.to_string(), + pool_id: astro_pool_id, + } + ] + ); + + let (_, pool_2) = helper + .create_and_seed_pair([ + coin(1_000_000_000000, "utest"), + coin(1_000_000_000000, "uusd"), + ]) + .unwrap(); + + // utest + // | + // ucoin -> uusd -> astro + helper + .set_pool_routes(vec![PoolRoute { + denom_in: "utest".to_string(), + denom_out: "uusd".to_string(), + pool_id: pool_2, + }]) + .unwrap(); + + let route = helper.query_route("utest", ASTRO_DENOM); + assert_eq!( + route, + vec![ + SwapRouteResponse { + pool_id: pool_2, + token_out_denom: "uusd".to_string(), + }, + SwapRouteResponse { + token_out_denom: ASTRO_DENOM.to_string(), + pool_id: astro_pool_id, + } + ] + ); + + let (_, pool_3) = helper + .create_and_seed_pair([ + coin(1_000_000_000000, "utest"), + coin(1_000_000_000000, "ucoin"), + ]) + .unwrap(); + + // Update route + // utest + // | + // ucoin -> uusd -> astro + helper + .set_pool_routes(vec![PoolRoute { + denom_in: "utest".to_string(), + denom_out: "ucoin".to_string(), + pool_id: pool_3, + }]) + .unwrap(); + + let route = helper.query_route("utest", ASTRO_DENOM); + assert_eq!( + route, + vec![ + SwapRouteResponse { + pool_id: pool_3, + token_out_denom: "ucoin".to_string(), + }, + SwapRouteResponse { + pool_id: pool_1, + token_out_denom: "uusd".to_string(), + }, + SwapRouteResponse { + token_out_denom: ASTRO_DENOM.to_string(), + pool_id: astro_pool_id, + } + ] + ); + + let (_, pool_4) = helper + .create_and_seed_pair([ + coin(1_000_000_000000, "utest"), + coin(1_000_000_000000, "uatomn"), + ]) + .unwrap(); + + // Trying to set route which doesn't lead to ASTRO + // utest -> uatomn + // x + // ucoin -> uusd -> astro + let err = helper + .set_pool_routes(vec![PoolRoute { + denom_in: "utest".to_string(), + denom_out: "uatomn".to_string(), + pool_id: pool_4, + }]) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::RouteNotFound { + denom: "uatomn".to_string(), + } + ); + + // Checking long swap path + let mut routes = (0..=MAX_SWAPS_DEPTH) + .into_iter() + .tuple_windows() + .map(|(i, j)| { + let coin_a = format!("coin{i}"); + let coin_b = format!("coin{j}"); + let (_, pool_id) = helper + .create_and_seed_pair([ + coin(1_000_000_000000, &coin_a), + coin(1_000_000_000000, &coin_b), + ]) + .unwrap(); + PoolRoute { + denom_in: coin_a, + denom_out: coin_b, + pool_id, + } + }) + .collect_vec(); + + let last_coin = format!("coin{MAX_SWAPS_DEPTH}"); + + let (_, pool_id) = helper + .create_and_seed_pair([ + coin(1_000_000_000000, &last_coin), + coin(1_000_000_000000, ASTRO_DENOM), + ]) + .unwrap(); + + routes.push(PoolRoute { + denom_in: last_coin, + denom_out: ASTRO_DENOM.to_string(), + pool_id, + }); + + let err = helper.set_pool_routes(routes).unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::FailedToBuildRoute { + denom: "coin0".to_string(), + route_taken: "coin0 -> coin1 -> coin2 -> coin3 -> coin4 -> coin5".to_string() + } + ); +} + +#[test] +fn test_collect() { + let owner = Addr::unchecked("owner"); + let mut helper = Helper::new(&owner).unwrap(); + + let (_, astro_pool_id) = helper + .create_and_seed_pair([ + coin(1_000_000_000000, "uusd"), + coin(1_000_000_000000, ASTRO_DENOM), + ]) + .unwrap(); + + helper + .set_pool_routes(vec![PoolRoute { + denom_in: "uusd".to_string(), + denom_out: ASTRO_DENOM.to_string(), + pool_id: astro_pool_id, + }]) + .unwrap(); + + // mock received fees + let maker = helper.maker.clone(); + helper.give_me_money(&[Asset::native("uusd", 1_000000u64)], &maker); + + helper + .collect(vec![CoinWithLimit { + denom: "uusd".to_string(), + amount: None, + }]) + .unwrap(); + + let uusd_bal = helper + .app + .wrap() + .query_balance(&helper.maker, "uusd") + .unwrap(); + assert_eq!(uusd_bal.amount.u128(), 0); + + let astro_bal = helper + .app + .wrap() + .query_balance(&helper.satellite, ASTRO_DENOM) + .unwrap(); + assert_eq!(astro_bal.amount.u128(), 998_048); + + let (_, pool_1) = helper + .create_and_seed_pair([ + coin(1_000_000_000000, "coin_a"), + coin(1_000_000_000000, "uusd"), + ]) + .unwrap(); + let (_, pool_2) = helper + .create_and_seed_pair([ + coin(1_000_000_000000, "coin_a"), + coin(1_000_000_000000, "coin_b"), + ]) + .unwrap(); + let (_, pool_3) = helper + .create_and_seed_pair([ + coin(1_000_000_000000, "coin_c"), + coin(1_000_000_000000, "uusd"), + ]) + .unwrap(); + + // Set routes + // coin_c + // | + // coin_b -> coin_a -> uusd -> astro + helper + .set_pool_routes(vec![ + PoolRoute { + denom_in: "coin_a".to_string(), + denom_out: "uusd".to_string(), + pool_id: pool_1, + }, + PoolRoute { + denom_in: "coin_b".to_string(), + denom_out: "coin_a".to_string(), + pool_id: pool_2, + }, + PoolRoute { + denom_in: "coin_c".to_string(), + denom_out: "uusd".to_string(), + pool_id: pool_3, + }, + ]) + .unwrap(); + + helper.give_me_money(&[Asset::native("coin_a", 1_000000u64)], &maker); + helper.give_me_money(&[Asset::native("coin_b", 1_000000u64)], &maker); + helper.give_me_money(&[Asset::native("coin_c", 1_000000u64)], &maker); + + helper + .collect(vec![ + CoinWithLimit { + denom: "coin_a".to_string(), + amount: None, + }, + CoinWithLimit { + denom: "coin_b".to_string(), + amount: None, + }, + CoinWithLimit { + denom: "coin_c".to_string(), + amount: None, + }, + ]) + .unwrap(); + + let coin_a_bal = helper + .app + .wrap() + .query_balance(&helper.maker, "coin_a") + .unwrap(); + assert_eq!(coin_a_bal.amount.u128(), 649); // tiny fee left after swaps + let coin_b_bal = helper + .app + .wrap() + .query_balance(&helper.maker, "coin_b") + .unwrap(); + assert_eq!(coin_b_bal.amount.u128(), 0); + let coin_c_bal = helper + .app + .wrap() + .query_balance(&helper.maker, "coin_c") + .unwrap(); + assert_eq!(coin_c_bal.amount.u128(), 0); + + // Satellite has received fees converted to astro + let astro_bal = helper + .app + .wrap() + .query_balance(&helper.satellite, ASTRO_DENOM) + .unwrap(); + assert_eq!(astro_bal.amount.u128(), 3_981818); + + // Check collect with limit + helper.give_me_money(&[Asset::native("coin_c", 1_000000u64)], &maker); + helper + .collect(vec![CoinWithLimit { + denom: "coin_c".to_string(), + amount: Some(500u128.into()), + }]) + .unwrap(); + let coin_c_bal = helper + .app + .wrap() + .query_balance(&helper.maker, "coin_c") + .unwrap(); + assert_eq!(coin_c_bal.amount.u128(), 999_500); + + // Try to set limit higher than balance + helper + .collect(vec![CoinWithLimit { + denom: "coin_c".to_string(), + amount: Some(1_000_000u128.into()), + }]) + .unwrap(); + let coin_c_bal = helper + .app + .wrap() + .query_balance(&helper.maker, "coin_c") + .unwrap(); + assert_eq!(coin_c_bal.amount.u128(), 0); + + // query all routes + let routes: Vec = helper + .app + .wrap() + .query_wasm_smart( + &helper.maker, + &astroport_on_osmosis::maker::QueryMsg::Routes { + start_after: None, + limit: Some(100), + }, + ) + .unwrap(); + assert_eq!( + routes, + vec![ + PoolRoute { + denom_in: "coin_a".to_string(), + denom_out: "uusd".to_string(), + pool_id: pool_1 + }, + PoolRoute { + denom_in: "coin_b".to_string(), + denom_out: "coin_a".to_string(), + pool_id: pool_2 + }, + PoolRoute { + denom_in: "coin_c".to_string(), + denom_out: "uusd".to_string(), + pool_id: pool_3 + }, + PoolRoute { + denom_in: "uusd".to_string(), + denom_out: "astro".to_string(), + pool_id: astro_pool_id + } + ] + ); + + let estimated_astro_out: Uint128 = helper + .app + .wrap() + .query_wasm_smart( + &helper.maker, + &astroport_on_osmosis::maker::QueryMsg::EstimateExactInSwap { + coin_in: coin(1_000000u128, "uusd"), + }, + ) + .unwrap(); + assert_eq!(estimated_astro_out.u128(), 997399); +} + +#[test] +fn update_owner() { + let owner = Addr::unchecked("owner"); + let mut helper = Helper::new(&owner).unwrap(); + + let new_owner = String::from("new_owner"); + + // New owner + let msg = ExecuteMsg::ProposeNewOwner { + owner: new_owner.clone(), + expires_in: 100, // seconds + }; + + // Unauthorized check + let err = helper + .app + .execute_contract( + Addr::unchecked("not_owner"), + helper.maker.clone(), + &msg, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + + // Claim before proposal + let err = helper + .app + .execute_contract( + Addr::unchecked(new_owner.clone()), + helper.maker.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Ownership proposal not found" + ); + + // Propose new owner + helper + .app + .execute_contract( + Addr::unchecked(&helper.owner), + helper.maker.clone(), + &msg, + &[], + ) + .unwrap(); + + // Claim from invalid addr + let err = helper + .app + .execute_contract( + Addr::unchecked("invalid_addr"), + helper.maker.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + + // Drop ownership proposal + let err = helper + .app + .execute_contract( + Addr::unchecked("invalid_addr"), + helper.maker.clone(), + &ExecuteMsg::DropOwnershipProposal {}, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + + helper + .app + .execute_contract( + helper.owner.clone(), + helper.maker.clone(), + &ExecuteMsg::DropOwnershipProposal {}, + &[], + ) + .unwrap(); + + // Propose new owner + helper + .app + .execute_contract( + Addr::unchecked(&helper.owner), + helper.maker.clone(), + &msg, + &[], + ) + .unwrap(); + + // Claim ownership + helper + .app + .execute_contract( + Addr::unchecked(new_owner.clone()), + helper.maker.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap(); + + let config: astroport_on_osmosis::maker::Config = helper + .app + .wrap() + .query_wasm_smart( + &helper.maker, + &astroport_on_osmosis::maker::QueryMsg::Config {}, + ) + .unwrap(); + assert_eq!(config.owner.to_string(), new_owner) +} diff --git a/e2e_tests/Cargo.toml b/e2e_tests/Cargo.toml index 25d6df3..962844f 100644 --- a/e2e_tests/Cargo.toml +++ b/e2e_tests/Cargo.toml @@ -8,8 +8,9 @@ anyhow = "1" cosmwasm-std = "1" serde = "1" osmosis-std = { workspace = true } -osmosis-test-tube = "21.0.0" -test-tube = "0.4.0" +osmosis-test-tube = "22.1" +test-tube = "0.5.0" astroport = { workspace = true } astroport-on-osmosis = { path = "../packages/astroport_on_osmosis", version = "1" } -serde_json = "1" \ No newline at end of file +serde_json = "1" +astro-satellite-package = "1" \ No newline at end of file diff --git a/e2e_tests/contracts/README.md b/e2e_tests/contracts/README.md index f2b81b5..3e982aa 100644 --- a/e2e_tests/contracts/README.md +++ b/e2e_tests/contracts/README.md @@ -1,2 +1,3 @@ Contracts which are not part of this repo but are used in the e2e tests. -1. astroport-native-coin-registry from [release v2.9.5](https://github.com/astroport-fi/astroport-core/releases/tag/v2.9.5) \ No newline at end of file +1. astroport-native-coin-registry from [Astroport core release v2.9.5](https://github.com/astroport-fi/astroport-core/releases/tag/v2.9.5) +2. astro-satellite from [Astroport ibc release v1.2.1](https://github.com/astroport-fi/astroport_ibc/releases/tag/v1.2.1) \ No newline at end of file diff --git a/e2e_tests/contracts/astro_satellite.wasm b/e2e_tests/contracts/astro_satellite.wasm new file mode 100644 index 0000000000000000000000000000000000000000..7b4493dc3ef1fd0af12868ac3d446ccc788b6829 GIT binary patch literal 314530 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

mD5+x| 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 z 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 ConcentratedPoolParams { + ConcentratedPoolParams { + amp: f64_to_dec(10f64), + gamma: f64_to_dec(0.000145), + mid_fee: f64_to_dec(0.0026), + out_fee: f64_to_dec(0.0045), + fee_gamma: f64_to_dec(0.00023), + repeg_profit_threshold: f64_to_dec(0.000002), + min_price_scale_delta: f64_to_dec(0.000146), + price_scale, + ma_half_time: 600, + track_asset_balances: None, + fee_share: None, + } +} const BUILD_CONTRACTS: &[&str] = &[ // "astroport-pcl-osmo", // we build this contract separately to hardcode factory address "astroport-factory-osmosis", + "astroport-maker-osmosis", ]; fn compile_wasm(project_dir: &str, contract: &str) { @@ -92,6 +107,9 @@ pub struct TestAppWrapper<'a> { pub code_ids: HashMap<&'a str, u64>, pub coin_registry: String, pub factory: String, + pub astro_denom: String, + pub maker: String, + pub satellite: String, } impl<'a> TestAppWrapper<'a> { @@ -106,6 +124,8 @@ impl<'a> TestAppWrapper<'a> { let target_dir = Path::new(&project_dir).join("target/wasm32-unknown-unknown/release"); let native_registry_wasm = Path::new(&project_dir).join("e2e_tests/contracts/astroport_native_coin_registry.wasm"); + let satellite_wasm = + Path::new(&project_dir).join("e2e_tests/contracts/astro_satellite.wasm"); let factory_wasm = target_dir.join("astroport_factory_osmosis.wasm"); let mut helper = Self { @@ -118,6 +138,9 @@ impl<'a> TestAppWrapper<'a> { code_ids: HashMap::new(), coin_registry: "".to_string(), factory: "".to_string(), + astro_denom: "".to_string(), + maker: "".to_string(), + satellite: "".to_string(), }; println!("Storing coin registry contract..."); @@ -141,8 +164,43 @@ impl<'a> TestAppWrapper<'a> { .unwrap(); helper.coin_registry = coin_registry_address.clone(); - // setting 3 a little hacky but I don't know other way - helper.code_ids.insert("pair-concentrated", 3); + helper.astro_denom = helper.register_and_mint("astro", 1_000_000_000_000, 6, None); + + println!("Storing satellite contract..."); + let satellite_code_id = helper.store_code(satellite_wasm).unwrap(); + helper.code_ids.insert("satellite", satellite_code_id); + let satellite_init_msg = astro_satellite_package::InstantiateMsg { + owner: helper.signer.address(), + astro_denom: helper.astro_denom.clone(), + transfer_channel: "channel-1".to_string(), + main_controller: "TBD".to_string(), + main_maker: "TBD".to_string(), + timeout: 360, + max_signal_outage: 1209600, + emergency_owner: helper.signer.address(), + }; + helper.satellite = helper + .init_contract("satellite", &satellite_init_msg, &[]) + .unwrap(); + + println!("Storing maker contract..."); + let maker_code_id = helper + .store_code(target_dir.join("astroport_maker_osmosis.wasm")) + .unwrap(); + helper.code_ids.insert("maker", maker_code_id); + + let maker_init_msg = astroport_on_osmosis::maker::InstantiateMsg { + owner: helper.signer.address(), + astro_denom: helper.astro_denom.to_owned(), + satellite: helper.satellite.to_owned(), + max_spread: Decimal::percent(10), + collect_cooldown: Some(60), + }; + + helper.maker = helper.init_contract("maker", &maker_init_msg, &[]).unwrap(); + + // setting 5 a little hacky but I don't know other way + helper.code_ids.insert("pair-concentrated", 5); let factory_init_msg = factory::InstantiateMsg { pair_configs: vec![PairConfig { @@ -154,7 +212,7 @@ impl<'a> TestAppWrapper<'a> { is_generator_disabled: false, permissioned: false, }], - fee_address: Some(FAKE_MAKER.to_string()), + fee_address: Some(helper.maker.clone()), generator_address: None, owner: helper.signer.address(), coin_registry_address, @@ -181,7 +239,7 @@ impl<'a> TestAppWrapper<'a> { UploadCosmWasmPoolCodeAndWhiteListProposal { title: String::from("store test cosmwasm pool code"), description: String::from("test"), - wasm_byte_code: std::fs::read(cl_pool_wasm).unwrap(), + wasm_byte_code: fs::read(cl_pool_wasm).unwrap(), }, helper.signer.address(), &helper.signer, diff --git a/e2e_tests/tests/maker_e2e_testing.rs b/e2e_tests/tests/maker_e2e_testing.rs new file mode 100644 index 0000000..5382785 --- /dev/null +++ b/e2e_tests/tests/maker_e2e_testing.rs @@ -0,0 +1,106 @@ +use astroport::asset::{Asset, AssetInfo}; +use cosmwasm_std::{coin, Decimal}; +use osmosis_test_tube::OsmosisTestApp; + +use astroport_osmo_e2e_tests::helper::{default_pcl_params, TestAppWrapper}; + +#[test] +fn collect_fees_test() { + let app = OsmosisTestApp::new(); + let helper = TestAppWrapper::bootstrap(&app).unwrap(); + + // Create and seed ASTRO pool + let uusd_denom = helper.register_and_mint("uusd", 200_000_000_000000, 6, None); + let (pair_addr, _) = helper + .create_pair( + &[ + AssetInfo::native(&helper.astro_denom), + AssetInfo::native(&uusd_denom), + ], + default_pcl_params(Decimal::one()), + ) + .unwrap(); + helper + .provide( + &helper.signer, + &pair_addr, + &[ + Asset::native(&helper.astro_denom, 1_000_000_000000u64), + Asset::native(&uusd_denom, 1_000_000_000000u64), + ], + None, + ) + .unwrap(); + let astro_pool = helper.get_pool_id_by_contract(&pair_addr); + + let ucoin_denom = helper.register_and_mint("ucoin", 200_000_000_000000, 6, None); + let (pair_addr, _) = helper + .create_pair( + &[ + AssetInfo::native(&ucoin_denom), + AssetInfo::native(&uusd_denom), + ], + default_pcl_params(Decimal::one()), + ) + .unwrap(); + helper + .provide( + &helper.signer, + &pair_addr, + &[ + Asset::native(&ucoin_denom, 1_000_000_000000u64), + Asset::native(&uusd_denom, 1_000_000_000000u64), + ], + None, + ) + .unwrap(); + let pool_1 = helper.get_pool_id_by_contract(&pair_addr); + + // Set routes + helper + .wasm + .execute( + &helper.maker, + &astroport_on_osmosis::maker::ExecuteMsg::SetPoolRoutes(vec![ + astroport_on_osmosis::maker::PoolRoute { + denom_in: ucoin_denom.clone(), + denom_out: uusd_denom.clone(), + pool_id: pool_1, + }, + astroport_on_osmosis::maker::PoolRoute { + denom_in: uusd_denom.clone(), + denom_out: helper.astro_denom.to_owned(), + pool_id: astro_pool, + }, + ]), + &[], + &helper.signer, + ) + .unwrap(); + + // Mock receiving fees + helper.mint(coin(1_000000, &ucoin_denom), Some(helper.maker.clone())); + + // Collect fees + let err = helper + .wasm + .execute( + &helper.maker, + &astroport_on_osmosis::maker::ExecuteMsg::Collect { + assets: vec![astroport_on_osmosis::maker::CoinWithLimit { + denom: ucoin_denom, + amount: None, + }], + }, + &[], + &helper.signer, + ) + .unwrap_err(); + // Assert we receive IBC error which means all other collect steps passed. + // test-tube doesn't support IBC thus it is correct to assert this error. + assert!( + err.to_string() + .contains("port ID (transfer) channel ID (channel-1): channel not found"), + "unexpected error: {err}" + ); +} diff --git a/e2e_tests/tests/e2e_testing.rs b/e2e_tests/tests/pcl_e2e_testing.rs similarity index 73% rename from e2e_tests/tests/e2e_testing.rs rename to e2e_tests/tests/pcl_e2e_testing.rs index 45af38d..9ce3a99 100644 --- a/e2e_tests/tests/e2e_testing.rs +++ b/e2e_tests/tests/pcl_e2e_testing.rs @@ -1,6 +1,5 @@ use astroport::asset::{native_asset_info, AssetInfo, AssetInfoExt}; use astroport::pair; -use astroport::pair_concentrated::ConcentratedPoolParams; use cosmwasm_std::{coin, to_json_binary, Coin, Decimal}; use osmosis_std::types::osmosis::cosmwasmpool::v1beta1::{ ContractInfoByPoolIdRequest, ContractInfoByPoolIdResponse, MsgCreateCosmWasmPool, @@ -8,77 +7,12 @@ use osmosis_std::types::osmosis::cosmwasmpool::v1beta1::{ }; use osmosis_test_tube::{Account, OsmosisTestApp, Runner}; -use astroport_osmo_e2e_tests::helper::{f64_to_dec, TestAppWrapper}; - -fn default_pcl_params() -> ConcentratedPoolParams { - ConcentratedPoolParams { - amp: f64_to_dec(10f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::from_ratio(1u8, 2u8), - ma_half_time: 600, - track_asset_balances: None, - fee_share: None, - } -} +use astroport_osmo_e2e_tests::helper::{default_pcl_params, TestAppWrapper}; fn gas_fee() -> Coin { coin(2_000_000_000000, "uosmo") } -#[test] -fn provide_withdraw_test() { - let app = OsmosisTestApp::new(); - let helper = TestAppWrapper::bootstrap(&app).unwrap(); - - let foo_denom = helper.register_and_mint("foo", 1_000_000_000000, 6, None); - let bar_denom = helper.register_and_mint("bar", 1_000_000_000000, 6, None); - let foo = native_asset_info(foo_denom.clone()); - let bar = native_asset_info(bar_denom.clone()); - - let (pair_addr, lp_token) = helper - .create_pair(&[foo.clone(), bar.clone()], default_pcl_params()) - .unwrap(); - - helper - .provide( - &helper.signer, - &pair_addr, - &[ - foo.with_balance(50_000_000000u128), - bar.with_balance(100_000_000000u128), - ], - None, - ) - .unwrap(); - - helper - .provide( - &helper.signer, - &pair_addr, - &[foo.with_balance(5_000_000000u128)], - None, - ) - .unwrap(); - - let lp_bal = helper.coin_balance(&helper.signer.address(), &lp_token); - - let foo_bal_before = helper.coin_balance(&helper.signer.address(), &foo_denom); - let bar_bal_before = helper.coin_balance(&helper.signer.address(), &bar_denom); - helper - .withdraw(&helper.signer, &pair_addr, coin(lp_bal, &lp_token)) - .unwrap(); - let foo_bal_after = helper.coin_balance(&helper.signer.address(), &foo_denom); - let bar_bal_after = helper.coin_balance(&helper.signer.address(), &bar_denom); - - assert_eq!(foo_bal_after - foo_bal_before, 54999_999257); - assert_eq!(bar_bal_after - bar_bal_before, 99999_998650); -} - #[test] fn dex_swap_test() { let app = OsmosisTestApp::new(); @@ -90,7 +24,10 @@ fn dex_swap_test() { let bar = native_asset_info(bar_denom.clone()); let (pair_addr, _) = helper - .create_pair(&[foo.clone(), bar.clone()], default_pcl_params()) + .create_pair( + &[foo.clone(), bar.clone()], + default_pcl_params(Decimal::from_ratio(1u8, 2u8)), + ) .unwrap(); let pool_id = helper.get_pool_id_by_contract(&pair_addr); @@ -172,7 +109,9 @@ fn init_outside_of_factory() { code_id: helper.code_ids["pair-concentrated"], instantiate_msg: to_json_binary(&pair::InstantiateMsg { asset_infos: vec![foo.clone(), bar.clone()], - init_params: Some(to_json_binary(&default_pcl_params()).unwrap()), + init_params: Some( + to_json_binary(&default_pcl_params(Decimal::from_ratio(1u8, 2u8))).unwrap(), + ), factory_addr: "".to_string(), token_code_id: 0, }) @@ -222,7 +161,10 @@ fn swap_with_fake_token() { let bar = native_asset_info(bar_denom.clone()); let (pair_addr, _) = helper - .create_pair(&[foo.clone(), bar.clone()], default_pcl_params()) + .create_pair( + &[foo.clone(), bar.clone()], + default_pcl_params(Decimal::from_ratio(1u8, 2u8)), + ) .unwrap(); let pool_id = helper.get_pool_id_by_contract(&pair_addr); diff --git a/packages/astroport_on_osmosis/Cargo.toml b/packages/astroport_on_osmosis/Cargo.toml index dcc1d88..5a3922c 100644 --- a/packages/astroport_on_osmosis/Cargo.toml +++ b/packages/astroport_on_osmosis/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-on-osmosis" -version = "1.0.1" +version = "1.1.0" authors = ["Astroport"] edition = "2021" description = "External API of Astroport contracts on Osmosis" diff --git a/packages/astroport_on_osmosis/src/lib.rs b/packages/astroport_on_osmosis/src/lib.rs index 28bc3ee..a9f036c 100644 --- a/packages/astroport_on_osmosis/src/lib.rs +++ b/packages/astroport_on_osmosis/src/lib.rs @@ -1 +1,2 @@ +pub mod maker; pub mod pair_pcl; diff --git a/packages/astroport_on_osmosis/src/maker.rs b/packages/astroport_on_osmosis/src/maker.rs new file mode 100644 index 0000000..8311956 --- /dev/null +++ b/packages/astroport_on_osmosis/src/maker.rs @@ -0,0 +1,117 @@ +use std::ops::RangeInclusive; + +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; + +/// Validation limit for max spread. From 0 to 50%. +pub const MAX_ALLOWED_SPREAD: Decimal = Decimal::percent(50); +/// Validations limits for cooldown period. From 30 to 600 seconds. +pub const COOLDOWN_LIMITS: RangeInclusive = 30..=600; +/// Maximum allowed route hops +pub const MAX_SWAPS_DEPTH: u8 = 5; + +/// Default pagination limit +pub const DEFAULT_PAGINATION_LIMIT: u32 = 50; + +#[cw_serde] +pub struct InstantiateMsg { + /// The contract's owner, who can update config + pub owner: String, + /// ASTRO denom + pub astro_denom: String, + /// Address which receives all swapped Astro. On Osmosis this is the address of the satellite contract + pub satellite: String, + /// The maximum spread used when swapping fee tokens to ASTRO + pub max_spread: Decimal, + /// If set defines the period when maker collect can be called + pub collect_cooldown: Option, +} + +#[cw_serde] +#[derive(Eq, Hash)] +pub struct PoolRoute { + pub denom_in: String, + pub denom_out: String, + pub pool_id: u64, +} + +#[cw_serde] +pub struct CoinWithLimit { + pub denom: String, + pub amount: Option, +} + +#[cw_serde] +pub enum ExecuteMsg { + /// Collects and swaps fee tokens to ASTRO + Collect { + /// Coins to swap to ASTRO. coin.amount is the amount to swap. If amount is omitted then whole balance will be used. + /// If amount is more than the balance, it will swap the whole balance. + assets: Vec, + }, + /// Updates general settings. Only the owner can execute this. + UpdateConfig { + /// ASTRO denom + astro_denom: Option, + /// Fee receiver address. + fee_receiver: Option, + /// The maximum spread used when swapping fee tokens to ASTRO + max_spread: Option, + /// Defines the period when maker collect can be called + collect_cooldown: Option, + }, + /// Configure specific pool ids for swapping asset_in to asset_out. + /// If route already exists, it will be overwritten. + SetPoolRoutes(Vec), + /// Creates a request to change the contract's ownership + ProposeNewOwner { + /// The newly proposed owner + owner: String, + /// The validity period of the proposal to change the owner + expires_in: u64, + }, + /// Removes a request to change contract ownership + DropOwnershipProposal {}, + /// Claims contract ownership + ClaimOwnership {}, +} + +#[cw_serde] +pub struct Config { + /// The contract's owner, who can update config + pub owner: Addr, + /// ASTRO denom + pub astro_denom: String, + /// Address which receives all swapped Astro. On Osmosis this is the address of the satellite contract + pub satellite: Addr, + /// The maximum spread used when swapping fee tokens to ASTRO + pub max_spread: Decimal, + /// If set defines the period when maker collect can be called + pub collect_cooldown: Option, +} + +#[cw_serde] +pub struct SwapRouteResponse { + pub pool_id: u64, + pub token_out_denom: String, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Query contract owner config + #[returns(Config)] + Config {}, + /// Get route for swapping an input denom into an output denom + #[returns(Vec)] + Route { denom_in: String, denom_out: String }, + /// List all maker routes + #[returns(Vec)] + Routes { + start_after: Option, + limit: Option, + }, + /// Return current spot price swapping In for Out + #[returns(Uint128)] + EstimateExactInSwap { coin_in: Coin }, +}