diff --git a/Cargo.lock b/Cargo.lock index 72335784..e5dd0f61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,15 +15,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "astroport" @@ -72,9 +72,9 @@ dependencies = [ [[package]] name = "astrovault" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00162d5a60a1463f5f35f7f5c5a60b01a7ab391693176028757559d90118a39" +checksum = "3a16c325554ef0760871dc4a0c2a3d527b5ba8559a4b7ad79a24753f1ba99805" dependencies = [ "bigint", "cosmwasm-schema", @@ -89,9 +89,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" [[package]] name = "base16ct" @@ -99,6 +105,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.7" @@ -127,6 +139,16 @@ dependencies = [ "crunchy 0.1.6", ] +[[package]] +name = "bincode2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49f6183038e081170ebbbadee6678966c7d54728938a3e7de7f4e780770318f" +dependencies = [ + "byteorder", + "serde", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -159,9 +181,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" dependencies = [ "serde", ] @@ -174,9 +196,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "num-traits", ] @@ -189,18 +211,18 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const_format" -version = "0.2.32" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" dependencies = [ "const_format_proc_macros", ] [[package]] name = "const_format_proc_macros" -version = "0.2.32" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" dependencies = [ "proc-macro2", "quote", @@ -231,32 +253,31 @@ dependencies = [ [[package]] name = "cosmwasm-crypto" -version = "1.5.5" +version = "1.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd50718a2b6830ce9eb5d465de5a018a12e71729d66b70807ce97e6dd14f931d" +checksum = "eba94b9f3fb79b9f1101b3e0c61995a557828e2c0d3f5579c1d0bfbea333c19e" dependencies = [ "digest 0.10.7", - "ecdsa", "ed25519-zebra", - "k256", + "k256 0.13.4", "rand_core 0.6.4", "thiserror", ] [[package]] name = "cosmwasm-derive" -version = "1.5.5" +version = "1.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "242e98e7a231c122e08f300d9db3262d1007b51758a8732cd6210b3e9faa4f3a" +checksum = "d67457e4acb04e738788d3489e343957455df2c4643f2b53050eb052ca631d19" dependencies = [ "syn 1.0.109", ] [[package]] name = "cosmwasm-schema" -version = "1.5.5" +version = "1.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7879036156092ad1c22fe0d7316efc5a5eceec2bc3906462a2560215f2a2f929" +checksum = "13bf06bf1c7ea737f6b3d955d9cabeb8cbbe4dcb8dea392e30f6fab4493a4b7a" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -267,9 +288,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.5.5" +version = "1.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb57855fbfc83327f8445ae0d413b1a05ac0d68c396ab4d122b2abd7bb82cb6" +checksum = "077fe378f16b54e3d0a57adb3f39a65bcf7bdbda6a5eade2f8ba7755c2fb1250" dependencies = [ "proc-macro2", "quote", @@ -278,11 +299,11 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.5.5" +version = "1.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c1556156fdf892a55cced6115968b961eaaadd6f724a2c2cb7d1e168e32dd3" +checksum = "3745e9fd9aad96236c3d6f1a6d844249ed3bb6b92fcdae16d8fe067c7a5121e8" dependencies = [ - "base64", + "base64 0.21.7", "bech32", "bnum", "cosmwasm-crypto", @@ -310,9 +331,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -329,6 +350,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -672,6 +705,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "der" version = "0.7.9" @@ -856,7 +899,7 @@ checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" [[package]] name = "drop-factory" version = "1.0.0" -source = "git+https://github.com/hadronlabs-org/drop-contracts.git?branch=feat/audit-fixes#a4da7691eb86e0c2364b9d4daccba9b1f036d2ab" +source = "git+https://github.com/hadronlabs-org/drop-contracts.git?branch=feat/audit-fixes#318f0d8d24e1f90f33987ff8da6e2e6f3c9fc62f" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -875,24 +918,27 @@ dependencies = [ [[package]] name = "drop-helpers" version = "1.0.0" -source = "git+https://github.com/hadronlabs-org/drop-contracts.git?branch=feat/audit-fixes#a4da7691eb86e0c2364b9d4daccba9b1f036d2ab" +source = "git+https://github.com/hadronlabs-org/drop-contracts.git?branch=feat/audit-fixes#318f0d8d24e1f90f33987ff8da6e2e6f3c9fc62f" dependencies = [ "cosmos-sdk-proto 0.20.0", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.2.0", + "hex", "neutron-sdk 0.10.0 (git+https://github.com/neutron-org/neutron-sdk?branch=feat/proposal-votes)", + "once_cell", "prost 0.12.6", "schemars", "serde", "serde-json-wasm 1.0.1", + "sha3", "thiserror", ] [[package]] name = "drop-macros" version = "1.0.0" -source = "git+https://github.com/hadronlabs-org/drop-contracts.git?branch=feat/audit-fixes#a4da7691eb86e0c2364b9d4daccba9b1f036d2ab" +source = "git+https://github.com/hadronlabs-org/drop-contracts.git?branch=feat/audit-fixes#318f0d8d24e1f90f33987ff8da6e2e6f3c9fc62f" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -901,10 +947,21 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "drop-proto" +version = "1.0.0" +source = "git+https://github.com/hadronlabs-org/drop-contracts.git?branch=feat/audit-fixes#318f0d8d24e1f90f33987ff8da6e2e6f3c9fc62f" +dependencies = [ + "cosmwasm-std", + "prost 0.12.6", + "prost-types 0.12.6", + "tendermint-proto 0.34.1", +] + [[package]] name = "drop-puppeteer-base" version = "1.0.0" -source = "git+https://github.com/hadronlabs-org/drop-contracts.git?branch=feat/audit-fixes#a4da7691eb86e0c2364b9d4daccba9b1f036d2ab" +source = "git+https://github.com/hadronlabs-org/drop-contracts.git?branch=feat/audit-fixes#318f0d8d24e1f90f33987ff8da6e2e6f3c9fc62f" dependencies = [ "cosmos-sdk-proto 0.20.0", "cosmwasm-schema", @@ -925,7 +982,7 @@ dependencies = [ [[package]] name = "drop-staking-base" version = "1.0.0" -source = "git+https://github.com/hadronlabs-org/drop-contracts.git?branch=feat/audit-fixes#a4da7691eb86e0c2364b9d4daccba9b1f036d2ab" +source = "git+https://github.com/hadronlabs-org/drop-contracts.git?branch=feat/audit-fixes#318f0d8d24e1f90f33987ff8da6e2e6f3c9fc62f" dependencies = [ "astroport 3.12.2", "cosmos-sdk-proto 0.20.0", @@ -938,6 +995,7 @@ dependencies = [ "cw721-base 0.18.0", "drop-helpers", "drop-macros", + "drop-proto", "drop-puppeteer-base", "neutron-sdk 0.10.0 (git+https://github.com/neutron-org/neutron-sdk?branch=feat/proposal-votes)", "optfield", @@ -953,18 +1011,30 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der 0.6.1", + "elliptic-curve 0.12.3", + "rfc6979 0.3.1", + "signature 1.6.4", +] + [[package]] name = "ecdsa" version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der", + "der 0.7.9", "digest 0.10.7", - "elliptic-curve", - "rfc6979", - "signature", - "spki", + "elliptic-curve 0.13.8", + "rfc6979 0.4.0", + "signature 2.2.0", + "spki 0.7.3", ] [[package]] @@ -988,25 +1058,55 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct 0.1.1", + "crypto-bigint 0.4.9", + "der 0.6.1", + "digest 0.10.7", + "ff 0.12.1", + "generic-array", + "group 0.12.1", + "pkcs8 0.9.0", + "rand_core 0.6.4", + "sec1 0.3.0", + "subtle", + "zeroize", +] + [[package]] name = "elliptic-curve" version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "base16ct", - "crypto-bigint", + "base16ct 0.2.0", + "crypto-bigint 0.5.5", "digest 0.10.7", - "ff", + "ff 0.13.0", "generic-array", - "group", - "pkcs8", + "group 0.13.0", + "pkcs8 0.10.2", "rand_core 0.6.4", - "sec1", + "sec1 0.7.3", "subtle", "zeroize", ] +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "ff" version = "0.13.0" @@ -1060,13 +1160,24 @@ dependencies = [ "wasi", ] +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff 0.12.1", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "group" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff", + "ff 0.13.0", "rand_core 0.6.4", "subtle", ] @@ -1103,8 +1214,8 @@ dependencies = [ [[package]] name = "hpl-interface" -version = "0.0.6" -source = "git+https://github.com/many-things/cw-hyperlane.git?branch=main#89d3943ee997d6da9b13fc3599eb62024b3bee67" +version = "0.0.7" +source = "git+https://github.com/many-things/cw-hyperlane.git?branch=main#7573576c97fe9ee9a91c3e4557ff5a32bfbcee40" dependencies = [ "bech32", "cosmwasm-schema", @@ -1128,7 +1239,7 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11c352715b36685c2543556a77091fb16af5d26257d5ce9c28e6756c1ccd71aa" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "flex-error", "ics23", @@ -1179,22 +1290,34 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "k256" -version = "0.13.1" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b" +dependencies = [ + "cfg-if", + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", + "sha2 0.10.8", +] + +[[package]] +name = "k256" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ "cfg-if", - "ecdsa", - "elliptic-curve", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", "once_cell", "sha2 0.10.8", - "signature", + "signature 2.2.0", ] [[package]] @@ -1214,9 +1337,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "lido-satellite" @@ -1261,7 +1384,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.95", ] [[package]] @@ -1289,7 +1412,7 @@ dependencies = [ "cosmwasm-std", "prost 0.12.6", "prost-types 0.12.6", - "protobuf 3.4.0", + "protobuf 3.7.1", "schemars", "serde", "serde-json-wasm 1.0.1", @@ -1310,7 +1433,7 @@ dependencies = [ "cosmwasm-std", "prost 0.12.6", "prost-types 0.12.6", - "protobuf 3.4.0", + "protobuf 3.7.1", "schemars", "serde", "serde-json-wasm 1.0.1", @@ -1348,9 +1471,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "opaque-debug" @@ -1366,7 +1489,7 @@ checksum = "fa59f025cde9c698fcb4fcb3533db4621795374065bee908215263488f2d2a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.95", ] [[package]] @@ -1403,14 +1526,24 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der 0.6.1", + "spki 0.6.0", +] + [[package]] name = "pkcs8" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.9", + "spki 0.7.3", ] [[package]] @@ -1421,9 +1554,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "predicates" -version = "3.1.2" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" dependencies = [ "anstyle", "predicates-core", @@ -1431,15 +1564,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" [[package]] name = "predicates-tree" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" dependencies = [ "predicates-core", "termtree", @@ -1447,9 +1580,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -1494,10 +1627,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.10.5", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.95", ] [[package]] @@ -1529,9 +1662,9 @@ dependencies = [ [[package]] name = "protobuf" -version = "3.4.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58678a64de2fced2bdec6bca052a6716a0efe692d6e3f53d1bda6a1def64cfc0" +checksum = "a3a7c64d9bf75b1b8d981124c14c179074e8caa7dfe7b6a12e6222ddcd0c8f72" dependencies = [ "bytes", "once_cell", @@ -1541,18 +1674,18 @@ dependencies = [ [[package]] name = "protobuf-support" -version = "3.4.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1ed294a835b0f30810e13616b1cd34943c6d1e84a8f3b0dcfe466d256c3e7e7" +checksum = "b088fd20b938a875ea00843b6faf48579462630015c3788d397ad6a786663252" dependencies = [ "thiserror", ] [[package]] name = "pryzm-std" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f30262f6c45bc128cb866a7c26f5ff9137e090eda1a29780eec48d32a2f51d" +checksum = "97d295897f6a1a3a05ef5b50794590bee0bc29cb2afb5d2117ef07aafd995d5a" dependencies = [ "chrono", "cosmwasm-std", @@ -1579,9 +1712,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -1601,6 +1734,17 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint 0.4.9", + "hmac", + "zeroize", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -1622,9 +1766,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "ryu" @@ -1653,7 +1797,21 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.72", + "syn 2.0.95", +] + +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct 0.1.1", + "der 0.6.1", + "generic-array", + "pkcs8 0.9.0", + "subtle", + "zeroize", ] [[package]] @@ -1662,25 +1820,158 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base16ct", - "der", + "base16ct 0.2.0", + "der 0.7.9", "generic-array", - "pkcs8", + "pkcs8 0.10.2", "subtle", "zeroize", ] +[[package]] +name = "secret-cosmwasm-crypto" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8535d61c88d0a6c222df2cebb69859d8e9ba419a299a1bc84c904b0d9c00c7b2" +dependencies = [ + "digest 0.10.7", + "ed25519-zebra", + "k256 0.11.6", + "rand_core 0.6.4", + "thiserror", +] + +[[package]] +name = "secret-cosmwasm-std" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e4393b01aa6587007161a6bb193859deaa8165ab06c8a35f253d329ff99e4d" +dependencies = [ + "base64 0.13.1", + "cosmwasm-derive", + "derivative", + "forward_ref", + "hex", + "schemars", + "secret-cosmwasm-crypto", + "serde", + "serde-json-wasm 0.4.1", + "thiserror", + "uint", +] + +[[package]] +name = "secret-cosmwasm-storage" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb43da2cb72a53b16ea1555bca794fb828b48ab24ebeb45f8e26f1881c45a783" +dependencies = [ + "secret-cosmwasm-std", + "serde", +] + +[[package]] +name = "secret-skip" +version = "0.3.0" +dependencies = [ + "cosmos-sdk-proto 0.19.0", + "cosmwasm-schema", + "ibc-proto", + "secret-cosmwasm-std", + "secret-storage-plus", + "secret-toolkit", + "thiserror", +] + +[[package]] +name = "secret-storage-plus" +version = "0.13.4" +source = "git+https://github.com/securesecrets/secret-plus-utils?tag=v0.1.1#96438a5bf7f1fb0acc540fe3a43e934f01e6711f" +dependencies = [ + "bincode2", + "schemars", + "secret-cosmwasm-std", + "serde", +] + +[[package]] +name = "secret-toolkit" +version = "0.10.0" +source = "git+https://github.com/scrtlabs/secret-toolkit?tag=v0.10.0#9e139bedab9eeb60f2bdbf9d4f1d2bb069886ea9" +dependencies = [ + "secret-toolkit-serialization", + "secret-toolkit-snip20", + "secret-toolkit-snip721", + "secret-toolkit-storage", + "secret-toolkit-utils", +] + +[[package]] +name = "secret-toolkit-serialization" +version = "0.10.0" +source = "git+https://github.com/scrtlabs/secret-toolkit?tag=v0.10.0#9e139bedab9eeb60f2bdbf9d4f1d2bb069886ea9" +dependencies = [ + "bincode2", + "schemars", + "secret-cosmwasm-std", + "serde", +] + +[[package]] +name = "secret-toolkit-snip20" +version = "0.10.0" +source = "git+https://github.com/scrtlabs/secret-toolkit?tag=v0.10.0#9e139bedab9eeb60f2bdbf9d4f1d2bb069886ea9" +dependencies = [ + "schemars", + "secret-cosmwasm-std", + "secret-toolkit-utils", + "serde", +] + +[[package]] +name = "secret-toolkit-snip721" +version = "0.10.0" +source = "git+https://github.com/scrtlabs/secret-toolkit?tag=v0.10.0#9e139bedab9eeb60f2bdbf9d4f1d2bb069886ea9" +dependencies = [ + "schemars", + "secret-cosmwasm-std", + "secret-toolkit-utils", + "serde", +] + +[[package]] +name = "secret-toolkit-storage" +version = "0.10.0" +source = "git+https://github.com/scrtlabs/secret-toolkit?tag=v0.10.0#9e139bedab9eeb60f2bdbf9d4f1d2bb069886ea9" +dependencies = [ + "secret-cosmwasm-std", + "secret-cosmwasm-storage", + "secret-toolkit-serialization", + "serde", +] + +[[package]] +name = "secret-toolkit-utils" +version = "0.10.0" +source = "git+https://github.com/scrtlabs/secret-toolkit?tag=v0.10.0#9e139bedab9eeb60f2bdbf9d4f1d2bb069886ea9" +dependencies = [ + "schemars", + "secret-cosmwasm-std", + "secret-cosmwasm-storage", + "serde", +] + [[package]] name = "semver" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" [[package]] name = "serde" -version = "1.0.204" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] @@ -1694,6 +1985,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde-json-wasm" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479b4dbc401ca13ee8ce902851b834893251404c4f3c65370a49e047a6be09a5" +dependencies = [ + "serde", +] + [[package]] name = "serde-json-wasm" version = "0.5.2" @@ -1723,13 +2023,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.95", ] [[package]] @@ -1740,14 +2040,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.95", ] [[package]] name = "serde_json" -version = "1.0.122" +version = "1.0.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" +checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" dependencies = [ "itoa", "memchr", @@ -1789,6 +2089,16 @@ dependencies = [ "keccak", ] +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "signature" version = "2.2.0" @@ -1891,6 +2201,44 @@ dependencies = [ "cw2 1.1.2", ] +[[package]] +name = "skip-go-secret-entry-point" +version = "0.3.0" +dependencies = [ + "cosmwasm-schema", + "schemars", + "secret-cosmwasm-std", + "secret-skip", + "secret-storage-plus", + "secret-toolkit", + "serde", + "serde_json", + "skip-go-swap-adapter-shade-protocol", + "test-case", + "thiserror", +] + +[[package]] +name = "skip-go-secret-ibc-adapter-ibc-hooks" +version = "0.3.0" +dependencies = [ + "cosmwasm-schema", + "cw2 1.1.2", + "cw20 1.1.2", + "ibc-proto", + "prost 0.11.9", + "schemars", + "secret-cosmwasm-std", + "secret-skip", + "secret-storage-plus", + "secret-toolkit", + "serde", + "serde-cw-value", + "serde-json-wasm 1.0.1", + "test-case", + "thiserror", +] + [[package]] name = "skip-go-swap-adapter-astroport" version = "0.3.0" @@ -2040,6 +2388,21 @@ dependencies = [ "thiserror", ] +[[package]] +name = "skip-go-swap-adapter-shade-protocol" +version = "0.3.0" +dependencies = [ + "cosmwasm-schema", + "schemars", + "secret-cosmwasm-std", + "secret-skip", + "secret-storage-plus", + "secret-toolkit", + "serde", + "test-case", + "thiserror", +] + [[package]] name = "skip-go-swap-adapter-white-whale" version = "0.3.0" @@ -2088,6 +2451,16 @@ dependencies = [ "strum_macros", ] +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der 0.6.1", +] + [[package]] name = "spki" version = "0.7.3" @@ -2095,7 +2468,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.9", ] [[package]] @@ -2123,7 +2496,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.72", + "syn 2.0.95", ] [[package]] @@ -2154,9 +2527,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.72" +version = "2.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" dependencies = [ "proc-macro2", "quote", @@ -2201,9 +2574,9 @@ dependencies = [ [[package]] name = "termtree" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "test-case" @@ -2223,7 +2596,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.95", ] [[package]] @@ -2234,35 +2607,35 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.95", "test-case-core", ] [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.95", ] [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "num-conv", @@ -2279,9 +2652,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", @@ -2307,15 +2680,15 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-xid" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "version_check" @@ -2342,7 +2715,7 @@ dependencies = [ "osmosis-std-derive", "prost 0.11.9", "prost-types 0.11.9", - "protobuf 3.4.0", + "protobuf 3.7.1", "schemars", "serde", "uint", diff --git a/Cargo.toml b/Cargo.toml index dfcdab6e..77aa31ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "contracts/entry-point", + "contracts/secret-entry-point", "contracts/adapters/hyperlane", "contracts/adapters/ibc/*", "contracts/adapters/swap/*", @@ -52,6 +53,7 @@ serde = { version = "1.0.194", default-features = false, features serde-cw-value = "0.7.0" serde-json-wasm = "1.0.1" skip = { version = "0.3.0", path = "./packages/skip" } +secret-skip = { version = "0.3.0", path = "./packages/secret-skip" } test-case = "3.3.1" thiserror = "1" white-whale-std = "1.1.1" diff --git a/contracts/adapters/ibc/secret-ibc-hooks/Cargo.toml b/contracts/adapters/ibc/secret-ibc-hooks/Cargo.toml new file mode 100644 index 00000000..019558fe --- /dev/null +++ b/contracts/adapters/ibc/secret-ibc-hooks/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "skip-go-secret-ibc-adapter-ibc-hooks" +version = { workspace = true } +rust-version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +documentation = { workspace = true } +keywords = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-schema = { workspace = true } +#cosmwasm-std = { workspace = true } +cw2 = { workspace = true } +cw20 = { workspace = true } +#cw-storage-plus = { workspace = true } +ibc-proto = { workspace = true } +prost = { workspace = true } +serde-json-wasm = { workspace = true } +serde-cw-value = { workspace = true } +# skip = { workspace = true } +thiserror = { workspace = true } + +secret-skip = { workspace = true } +serde = "1.0.114" +schemars = "0.8.1" +secret-toolkit = { git = "https://github.com/scrtlabs/secret-toolkit", tag = "v0.10.0" } +secret-storage-plus = { git = "https://github.com/securesecrets/secret-plus-utils", tag = "v0.1.1", features = [] } +cosmwasm-std = { package = "secret-cosmwasm-std", version = "1.1.11"} + +[dev-dependencies] +test-case = { workspace = true } diff --git a/contracts/adapters/ibc/secret-ibc-hooks/README.md b/contracts/adapters/ibc/secret-ibc-hooks/README.md new file mode 100644 index 00000000..f2b526a8 --- /dev/null +++ b/contracts/adapters/ibc/secret-ibc-hooks/README.md @@ -0,0 +1,68 @@ +# Secret IBC Transfer Adapter Contract + +The Osmosis IBC Transfer adapter contract is responsible for: +1. Dispatching the IBC transfer. +2. Failing the entire transaction if the IBC transfer errors on the swap chain (sending the caller back their original funds). +3. Refunding the caller on the swap chain if the IBC transfer errors or times out once it reaches the destination chain. + +WARNING: Do not send funds directly to the contract without calling one of its functions. Funds sent directly to the contract do not trigger any contract logic that performs validation / safety checks (as the Cosmos SDK handles direct fund transfers in the `Bank` module and not the `Wasm` module). There are no explicit recovery mechanisms for accidentally sent funds. + +## InstantiateMsg + +Instantiates a new Osmosis IBC Transfer adapter contract. + +``` json +{} +``` + +## ExecuteMsg + +### `ibc_transfer` + +Dispatches an ICS-20 IBC Transfer given the parameters provided in the contract call. + +Note: Fees sent as parameters with the contract call are unused by the contract since Osmosis currently does not require ICS-29 fees for outgoing ibc transfers. The fee field is still included in the call data to keep the interface the same across all IBC transfer adapter contracts. + +``` json +{ + "ibc_transfer": { + "info": { + "source_channel": "channel-1", + "receiver": "cosmos...", + "fee": { + "recv_fee": [], + "ack_fee": [], + "timeout_fee": [] + }, + "memo": "", + "recover_address": "osmo..." + }, + "coin": { + "denom": "uosmo", + "amount": "1000000" + }, + "timeout_timestamp": 1000000000000 + } +} +``` + +## QueryMsg + +### `in_progress_recover_address` + +Returns the in progress recover address associated with the given `channel_id` and `sequence_id` (which make up a unique identifier mapped to in progress ibc transfers in the sub msg reply handler). + +Query: +``` json +{ + "in_progress_recover_address": { + "channel_id": "channel-1", + "sequence_id": 420 + } +} +``` + +Response: +``` json +"osmo..." +``` \ No newline at end of file diff --git a/contracts/adapters/ibc/secret-ibc-hooks/src/bin/ibc-hooks-schema.rs b/contracts/adapters/ibc/secret-ibc-hooks/src/bin/ibc-hooks-schema.rs new file mode 100644 index 00000000..3c4f61ad --- /dev/null +++ b/contracts/adapters/ibc/secret-ibc-hooks/src/bin/ibc-hooks-schema.rs @@ -0,0 +1,10 @@ +use cosmwasm_schema::write_api; +use secret_skip::ibc::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg + } +} diff --git a/contracts/adapters/ibc/secret-ibc-hooks/src/contract.rs b/contracts/adapters/ibc/secret-ibc-hooks/src/contract.rs new file mode 100644 index 00000000..3a079475 --- /dev/null +++ b/contracts/adapters/ibc/secret-ibc-hooks/src/contract.rs @@ -0,0 +1,497 @@ +use crate::{ + error::{ContractError, ContractResult}, + state::{ + ACK_ID_TO_RECOVER_ADDRESS, ENTRY_POINT_CONTRACT, ICS20_CONTRACT, IN_PROGRESS_CHANNEL_ID, + IN_PROGRESS_RECOVER_ADDRESS, REGISTERED_TOKENS, VIEWING_KEY, + }, +}; +use cosmwasm_std::{ + entry_point, from_binary, to_binary, BankMsg, Binary, ContractInfo, Deps, DepsMut, Env, + MessageInfo, Reply, Response, SubMsg, SubMsgResult, +}; +use serde_cw_value::Value; +// use cw2::set_contract_version; +use ibc_proto::ibc::applications::transfer::v1::MsgTransferResponse; +use prost::Message; +use secret_skip::{ + asset::Asset, + cw20::Cw20Coin, + ibc::{ + AckID, ExecuteMsg, IbcInfo, IbcLifecycleComplete, Ics20TransferMsg, InstantiateMsg, + MigrateMsg, QueryMsg, Snip20HookMsg, + }, + snip20::Snip20ReceiveMsg, + sudo::{OsmosisSudoMsg as SudoMsg, SudoType}, +}; +use secret_toolkit::snip20; + +// const IBC_MSG_TRANSFER_TYPE_URL: &str = "/ibc.applications.transfer.v1.MsgTransfer"; +const REPLY_ID: u64 = 1; + +/////////////// +/// MIGRATE /// +/////////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> ContractResult { + // Set contract version + // set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + // Validate entry point contract address + let checked_entry_point_contract = ContractInfo { + address: deps + .api + .addr_validate(&msg.entry_point_contract.address.to_string())?, + code_hash: msg.entry_point_contract.code_hash, + }; + + // Store the entry point contract address + ENTRY_POINT_CONTRACT.save(deps.storage, &checked_entry_point_contract)?; + + let checked_ics20_contract = ContractInfo { + address: deps + .api + .addr_validate(&msg.ics20_contract.address.to_string())?, + code_hash: msg.ics20_contract.code_hash, + }; + + ICS20_CONTRACT.save(deps.storage, &checked_ics20_contract)?; + + Ok(Response::new() + .add_attribute("action", "migrate") + .add_attribute( + "entry_point_contract_address", + checked_entry_point_contract.address.to_string(), + ) + .add_attribute( + "ics20_contract_address", + checked_ics20_contract.address.to_string(), + )) +} + +/////////////////// +/// INSTANTIATE /// +/////////////////// + +// Contract name and version used for migration. +/* +const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +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, +) -> ContractResult { + // Set contract version + + // Validate entry point contract address + let checked_entry_point_contract = ContractInfo { + address: deps + .api + .addr_validate(&msg.entry_point_contract.address.to_string())?, + code_hash: msg.entry_point_contract.code_hash, + }; + + // Store the entry point contract address + ENTRY_POINT_CONTRACT.save(deps.storage, &checked_entry_point_contract)?; + + let checked_ics20_contract = ContractInfo { + address: deps + .api + .addr_validate(&msg.ics20_contract.address.to_string())?, + code_hash: msg.ics20_contract.code_hash, + }; + + ICS20_CONTRACT.save(deps.storage, &checked_ics20_contract)?; + + Ok(Response::new() + .add_attribute("action", "instantiate") + .add_attribute( + "entry_point_contract_address", + checked_entry_point_contract.address.to_string(), + ) + .add_attribute( + "ics20_contract_address", + checked_ics20_contract.address.to_string(), + )) +} + +/////////////// +/// EXECUTE /// +/////////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> ContractResult { + match msg { + ExecuteMsg::Receive(msg) => receive_snip20(deps, env, info, msg), + ExecuteMsg::RegisterTokens { contracts } => register_tokens(deps, env, contracts), + _ => Err(ContractError::UnsupportedExecuteMsg), + } +} + +pub fn receive_snip20( + deps: DepsMut, + env: Env, + mut info: MessageInfo, + snip20_msg: Snip20ReceiveMsg, +) -> ContractResult { + let sent_asset = Asset::Cw20(Cw20Coin { + address: info.sender.to_string(), + amount: snip20_msg.amount.u128().into(), + }); + + // Set the sender to the originating address that triggered the snip20 send call + // This is later validated / enforced to be the entry point contract address + info.sender = deps.api.addr_validate(&snip20_msg.sender.to_string())?; + match snip20_msg.msg { + Some(msg) => match from_binary(&msg)? { + // Transfer tokens out over ICS20 + Snip20HookMsg::IbcTransfer { + info: ibc_info, + timeout_timestamp, + } => { + execute_ics20_ibc_transfer(deps, env, info, ibc_info, sent_asset, timeout_timestamp) + } + }, + None => Err(ContractError::NoSnip20ReceiveMsg), + } +} + +fn execute_ics20_ibc_transfer( + deps: DepsMut, + env: Env, + info: MessageInfo, + ibc_info: IbcInfo, + sent_asset: Asset, + timeout_timestamp: u64, +) -> ContractResult { + // Get entry point contract address from storage + let entry_point_contract = ENTRY_POINT_CONTRACT.load(deps.storage)?; + + let ics20_contract = ICS20_CONTRACT.load(deps.storage)?; + + // Enforce the caller is the entry point contract + if info.sender != entry_point_contract.address { + return Err(ContractError::Unauthorized); + } + + // Error if ibc_info.fee is not None since Secret does not support fees + if ibc_info.fee.is_some() { + return Err(ContractError::IbcFeesNotSupported); + } + + let sent_asset_contract = + REGISTERED_TOKENS.load(deps.storage, deps.api.addr_validate(sent_asset.denom())?)?; + + // Verify memo is valid json and add the necessary key/value pair to trigger the ibc hooks callback logic. + let memo = verify_and_create_memo(ibc_info.memo, env.contract.address.to_string())?; + + println!("memo {}", memo); + let ibc_transfer_msg = match snip20::send_msg_with_code_hash( + ics20_contract.address.to_string(), + Some(ics20_contract.code_hash), + sent_asset.amount(), + Some(to_binary(&Ics20TransferMsg { + channel: ibc_info.source_channel.clone(), + remote_address: ibc_info.receiver, + timeout: Some(timeout_timestamp), + })?), + Some(memo), + None, + 0, + sent_asset_contract.code_hash.clone(), + sent_asset_contract.address.to_string(), + ) { + Ok(msg) => msg, + Err(e) => return Err(ContractError::Std(e)), + }; + + // Save in progress recover address to storage, to be used in sudo handler + IN_PROGRESS_RECOVER_ADDRESS.save( + deps.storage, + &ibc_info.recover_address, // This address is verified in entry point + )?; + + // Save in progress channel id to storage, to be used in sudo handler + IN_PROGRESS_CHANNEL_ID.save(deps.storage, &ibc_info.source_channel)?; + + // Create sub message from ICS20 send message to receive a reply + let sub_msg = SubMsg::reply_on_success(ibc_transfer_msg, REPLY_ID); + + Ok(Response::new() + .add_submessage(sub_msg) + .add_attribute("action", "execute_ics20_ibc_transfer")) +} + +/* +// Converts the given info and coin into an ibc transfer message, +// saves necessary info in case the ibc transfer fails to send funds back to +// a recovery address, and then emits the ibc transfer message as a sub message +fn execute_ibc_transfer( + deps: DepsMut, + env: Env, + info: MessageInfo, + ibc_info: IbcInfo, + coin: Coin, + timeout_timestamp: u64, +) -> ContractResult { + // Get entry point contract address from storage + let entry_point_contract = ENTRY_POINT_CONTRACT.load(deps.storage)?; + + // Enforce the caller is the entry point contract + if info.sender != entry_point_contract.address { + return Err(ContractError::Unauthorized); + } + + // Error if ibc_info.fee is not None since Osmosis does not support fees + if ibc_info.fee.is_some() { + return Err(ContractError::IbcFeesNotSupported); + } + + // Save in progress recover address to storage, to be used in sudo handler + IN_PROGRESS_RECOVER_ADDRESS.save( + deps.storage, + &ibc_info.recover_address, // This address is verified in entry point + )?; + + // Save in progress channel id to storage, to be used in sudo handler + IN_PROGRESS_CHANNEL_ID.save(deps.storage, &ibc_info.source_channel)?; + + // Verify memo is valid json and add the necessary key/value pair to trigger the ibc hooks callback logic. + let memo = verify_and_create_memo(ibc_info.memo, env.contract.address.to_string())?; + + // Create osmosis ibc transfer message + let msg = MsgTransfer { + source_port: "transfer".to_string(), + source_channel: ibc_info.source_channel, + token: Some(ProtoCoin(coin).into()), + sender: env.contract.address.to_string(), + receiver: ibc_info.receiver, + timeout_height: None, + timeout_timestamp, + memo, + }; + + // Create stargate message from osmosis ibc transfer message + let msg = CosmosMsg::Stargate { + type_url: IBC_MSG_TRANSFER_TYPE_URL.to_string(), + value: msg.encode_to_vec().into(), + }; + + // Create sub message from osmosis ibc transfer message to receive a reply + let sub_msg = SubMsg::reply_on_success(msg, REPLY_ID); + + Ok(Response::new() + .add_submessage(sub_msg) + .add_attribute("action", "execute_ibc_transfer")) +} +*/ + +///////////// +/// REPLY /// +///////////// + +// Handles the reply from the ibc transfer sub message +// Upon success, maps the sub msg AckID (channel_id, sequence_id) +// to the in progress ibc transfer struct, and saves it to storage. +// Now that the map entry is stored, it also removes the in progress +// ibc transfer from storage. +#[entry_point] +pub fn reply(deps: DepsMut, _env: Env, reply: Reply) -> ContractResult { + // Error if the reply id is not the same as the one used in the sub message dispatched + // This should never happen since we are using a constant reply id, but added in case + // the wasm module doesn't behave as expected. + if reply.id != REPLY_ID { + unreachable!() + } + + // Get the sub message response from the reply and error if it does not exist + // This should never happen since sub msg was set to reply on success only, + // but added in case the wasm module doesn't behave as expected. + let SubMsgResult::Ok(sub_msg_response) = reply.result else { + unreachable!() + }; + + // Parse the response from the sub message + let resp: MsgTransferResponse = MsgTransferResponse::decode( + sub_msg_response + .data + .ok_or(ContractError::MissingResponseData)? + .as_slice(), + )?; + + // Get and delete the in progress recover address from storage + let in_progress_recover_address = IN_PROGRESS_RECOVER_ADDRESS.load(deps.storage)?; + IN_PROGRESS_RECOVER_ADDRESS.remove(deps.storage); + println!("IN PROG {}", in_progress_recover_address); + + // Get and delete the in progress channel id from storage + let in_progress_channel_id = IN_PROGRESS_CHANNEL_ID.load(deps.storage)?; + IN_PROGRESS_CHANNEL_ID.remove(deps.storage); + + // Set ack_id to be the channel id and sequence id from the response as a tuple + let ack_id: AckID = (&in_progress_channel_id, resp.sequence); + println!("ACKID {:?}", ack_id); + + // Error if unique ack_id (channel id, sequence id) already exists in storage + if ACK_ID_TO_RECOVER_ADDRESS.has(deps.storage, ack_id) { + return Err(ContractError::AckIDAlreadyExists { + channel_id: ack_id.0.into(), + sequence_id: ack_id.1, + }); + } + + // Set the in progress recover address to storage, keyed by channel id and sequence id + ACK_ID_TO_RECOVER_ADDRESS.save(deps.storage, ack_id, &in_progress_recover_address)?; + + Ok(Response::new().add_attribute("action", "sub_msg_reply_success")) +} + +//////////// +/// SUDO /// +//////////// + +// Handles the ibc callback from the ibc hooks module +// Upon success, removes the in progress ibc transfer from storage and returns immediately. +// Upon error or timeout, sends the attempted ibc transferred funds back to the user's recover address. +#[entry_point] +pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> ContractResult { + // Get the channel id, sequence id, and sudo type from the sudo message + let (channel, sequence, sudo_type) = match msg { + SudoMsg::IbcLifecycleComplete(IbcLifecycleComplete::IbcAck { + channel, + sequence, + ack: _, + success, + }) => { + // Remove the AckID <> in progress ibc transfer from storage + // and return immediately if the ibc transfer was successful + // since no further action is needed. + if success { + let ack_id: AckID = (&channel, sequence); + ACK_ID_TO_RECOVER_ADDRESS.remove(deps.storage, ack_id); + + return Ok(Response::new().add_attribute("action", SudoType::Response)); + } + + (channel, sequence, SudoType::Error) + } + SudoMsg::IbcLifecycleComplete(IbcLifecycleComplete::IbcTimeout { channel, sequence }) => { + (channel, sequence, SudoType::Timeout) + } + }; + + // Get and remove the AckID <> in progress recover address from storage + let ack_id: AckID = (&channel, sequence); + let to_address = ACK_ID_TO_RECOVER_ADDRESS.load(deps.storage, ack_id)?; + ACK_ID_TO_RECOVER_ADDRESS.remove(deps.storage, ack_id); + + // Get all coins from contract's balance, which will be the the + // failed ibc transfer coin and any leftover dust on the contract + let amount = deps.querier.query_all_balances(env.contract.address)?; + + // If amount is empty, return a no funds to refund error + if amount.is_empty() { + return Err(ContractError::NoFundsToRefund); + } + + // Create bank send message to send funds back to user's recover address + let bank_send_msg = BankMsg::Send { to_address, amount }; + + Ok(Response::new() + .add_message(bank_send_msg) + .add_attribute("action", sudo_type)) +} + +//////////////////////// +/// HELPER FUNCTIONS /// +//////////////////////// + +// Verifies the given memo is empty or valid json, and then adds the necessary +// key/value pair to trigger the ibc hooks callback logic. +fn verify_and_create_memo(memo: String, contract_address: String) -> ContractResult { + // If the memo given is empty, then set it to "{}" to avoid json parsing errors. Then, + // get Value object from json string, erroring if the memo was not null while not being valid json + let mut memo: Value = serde_json_wasm::from_str(if memo.is_empty() { "{}" } else { &memo })?; + + // Transform the Value object into a Value map representation of the json string + // and insert the necessary key value pair into the memo map to trigger + // the ibc hooks callback logic. That key value pair is: + // { "ibc_callback": } + // + // If the "ibc_callback" key was already set, this will override + // the value with the current contract address. + if let Value::Map(ref mut memo) = memo { + memo.insert( + Value::String("ibc_callback".to_string()), + Value::String(contract_address), + ) + } else { + unreachable!() + }; + + // Transform the memo Value map back into a json string + let memo = serde_json_wasm::to_string(&memo)?; + + Ok(memo) +} + +fn register_tokens( + deps: DepsMut, + env: Env, + contracts: Vec, +) -> ContractResult { + let mut response = Response::new(); + + let viewing_key = VIEWING_KEY.load(deps.storage)?; + + for contract in contracts.iter() { + // Add to storage for later use of code hash + REGISTERED_TOKENS.save(deps.storage, contract.address.clone(), contract)?; + // register receive, set viewing key, & add attribute + response = response + .add_attribute("register_token", contract.address.clone()) + .add_messages(vec![ + snip20::set_viewing_key_msg( + viewing_key.clone(), + None, + 255, + contract.code_hash.clone(), + contract.address.to_string(), + )?, + snip20::register_receive_msg( + env.contract.code_hash.clone(), + None, + 255, + contract.code_hash.clone(), + contract.address.to_string(), + )?, + ]); + } + + Ok(response) +} + +///////////// +/// QUERY /// +///////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> ContractResult { + match msg { + QueryMsg::InProgressRecoverAddress { + channel_id, + sequence_id, + } => to_binary(&ACK_ID_TO_RECOVER_ADDRESS.load(deps.storage, (&channel_id, sequence_id))?), + } + .map_err(From::from) +} diff --git a/contracts/adapters/ibc/secret-ibc-hooks/src/error.rs b/contracts/adapters/ibc/secret-ibc-hooks/src/error.rs new file mode 100644 index 00000000..6bbce185 --- /dev/null +++ b/contracts/adapters/ibc/secret-ibc-hooks/src/error.rs @@ -0,0 +1,46 @@ +use cosmwasm_std::{OverflowError, StdError}; +use thiserror::Error; + +pub type ContractResult = core::result::Result; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error(transparent)] + Decode(#[from] prost::DecodeError), + + #[error(transparent)] + JsonDecode(#[from] serde_json_wasm::de::Error), + + #[error(transparent)] + JsonEncode(#[from] serde_json_wasm::ser::Error), + + #[error(transparent)] + Overflow(#[from] OverflowError), + + #[error("IBC fees are not supported, vectors must be empty")] + IbcFeesNotSupported, + + #[error("SubMsgResponse does not contain data")] + MissingResponseData, + + #[error("Failed to receive ibc funds to refund the user")] + NoFundsToRefund, + + #[error("Unauthorized")] + Unauthorized, + + #[error("ACK ID already exists for channel ID {channel_id} and sequence ID {sequence_id}")] + AckIDAlreadyExists { + channel_id: String, + sequence_id: u64, + }, + + #[error("No Snip20 Receive Message sent")] + NoSnip20ReceiveMsg, + + #[error("Unsupported Execute Message")] + UnsupportedExecuteMsg, +} diff --git a/contracts/adapters/ibc/secret-ibc-hooks/src/lib.rs b/contracts/adapters/ibc/secret-ibc-hooks/src/lib.rs new file mode 100644 index 00000000..3d3e89c8 --- /dev/null +++ b/contracts/adapters/ibc/secret-ibc-hooks/src/lib.rs @@ -0,0 +1,3 @@ +pub mod contract; +pub mod error; +pub mod state; diff --git a/contracts/adapters/ibc/secret-ibc-hooks/src/state.rs b/contracts/adapters/ibc/secret-ibc-hooks/src/state.rs new file mode 100644 index 00000000..2268b8d3 --- /dev/null +++ b/contracts/adapters/ibc/secret-ibc-hooks/src/state.rs @@ -0,0 +1,14 @@ +use cosmwasm_std::{Addr, ContractInfo}; +use secret_skip::ibc::AckID; +use secret_storage_plus::{Item, Map}; + +pub const ENTRY_POINT_CONTRACT: Item = Item::new("entry_point_contract_address"); +pub const IN_PROGRESS_RECOVER_ADDRESS: Item = Item::new("in_progress_recover_address"); +pub const IN_PROGRESS_CHANNEL_ID: Item = Item::new("in_progress_channel_id"); +pub const ACK_ID_TO_RECOVER_ADDRESS: Map = Map::new("ack_id_to_recover_address"); + +pub const ICS20_CONTRACT: Item = Item::new("ics20_contract"); + +pub const REGISTERED_TOKENS: Map = Map::new("registered_tokens"); + +pub const VIEWING_KEY: Item = Item::new("viewing_key"); diff --git a/contracts/adapters/ibc/secret-ibc-hooks/tests/test_execute_ibc_transfer.rs b/contracts/adapters/ibc/secret-ibc-hooks/tests/test_execute_ibc_transfer.rs new file mode 100644 index 00000000..733d0f75 --- /dev/null +++ b/contracts/adapters/ibc/secret-ibc-hooks/tests/test_execute_ibc_transfer.rs @@ -0,0 +1,344 @@ +use cosmwasm_std::{ + to_binary, + WasmMsg, + testing::{mock_dependencies, mock_env, mock_info}, + Addr, Coin, ContractInfo, + ReplyOn::Success, + SubMsg, +}; +use ibc_proto::cosmos::base::v1beta1::Coin as IbcCoin; +use ibc_proto::ibc::applications::transfer::v1::MsgTransfer; +use prost::Message; +use secret_skip::{ + asset::Asset, + ibc::{ExecuteMsg, IbcFee, IbcInfo, Snip20HookMsg, + Ics20TransferMsg}, + snip20::{self, Snip20ReceiveMsg}, + cw20::Cw20Coin, +}; +use skip_go_secret_ibc_adapter_ibc_hooks::{ + error::ContractResult, + state::{ + ENTRY_POINT_CONTRACT, ICS20_CONTRACT, IN_PROGRESS_CHANNEL_ID, + IN_PROGRESS_RECOVER_ADDRESS, REGISTERED_TOKENS, VIEWING_KEY, + }, +}; +use test_case::test_case; + +/* +Test Cases: + +Expect Response (Output Message Is Correct, In Progress Ibc Transfer Is Saved, No Error) + - Empty String Memo + - Override Already Set Ibc Callback Memo + - Add Ibc Callback Key/Value Pair To Other Key/Value In Memo + +Expect Error + - Unauthorized Caller (Only the stored entry point contract can call this function) + - Non Empty String, Invalid Json Memo + - Non Empty IBC Fees, IBC Fees Not Supported + + */ + +// Define test parameters +struct Params { + caller: String, + ibc_adapter_contract_address: Addr, + sent_asset: Asset, + ibc_info: IbcInfo, + timeout_timestamp: u64, + expected_messages: Vec, + expected_error_string: String, +} + +// Test execute_ibc_transfer +#[test_case( + Params { + caller: "entry_point".to_string(), + ibc_adapter_contract_address: Addr::unchecked("ibc_transfer".to_string()), + sent_asset: Asset::Cw20(Cw20Coin { + address: "secret123".to_string(), + amount: 100u128.into(), + }), + ibc_info: IbcInfo { + source_channel: "source_channel".to_string(), + receiver: "receiver".to_string(), + fee: None, + memo: "".to_string(), + recover_address: "recover_address".to_string(), + }, + timeout_timestamp: 100, + expected_messages: vec![SubMsg { + id: 1, + msg: WasmMsg::Execute { + contract_addr: "secret123".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&snip20::ExecuteMsg::Send { + amount: 100u128.into(), + recipient: "ics20".to_string(), + recipient_code_hash: Some("code_hash".to_string()), + memo: Some(r#"{"ibc_callback":"ibc_transfer"}"#.to_string()), + padding: None, + msg: Some(to_binary(&Ics20TransferMsg { + channel: "source_channel".to_string(), + remote_address: "receiver".to_string(), + timeout: Some(100), + })?), + })?, + funds: vec![], + }.into(), + gas_limit: None, + reply_on: Success, + }], + expected_error_string: "".to_string(), + }; + "Empty String Memo")] +#[test_case( + Params { + caller: "entry_point".to_string(), + ibc_adapter_contract_address: Addr::unchecked("ibc_transfer".to_string()), + sent_asset: Asset::Cw20(Cw20Coin { + address: "secret123".to_string(), + amount: 100u128.into(), + }), + ibc_info: IbcInfo { + source_channel: "source_channel".to_string(), + receiver: "receiver".to_string(), + fee: None, + memo: r#"{"ibc_callback":"random_address"}"#.to_string(), + recover_address: "recover_address".to_string(), + }, + timeout_timestamp: 100, + expected_messages: vec![SubMsg { + id: 1, + msg: WasmMsg::Execute { + contract_addr: "secret123".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&snip20::ExecuteMsg::Send { + amount: 100u128.into(), + recipient: "ics20".to_string(), + recipient_code_hash: Some("code_hash".to_string()), + memo: Some(r#"{"ibc_callback":"ibc_transfer"}"#.to_string()), + padding: None, + msg: Some(to_binary(&Ics20TransferMsg { + channel: "source_channel".to_string(), + remote_address: "receiver".to_string(), + timeout: Some(100), + })?), + })?, + funds: vec![], + }.into(), + gas_limit: None, + reply_on: Success, + } + ], + expected_error_string: "".to_string(), + }; + "Override Already Set Ibc Callback Memo")] +#[test_case( + Params { + caller: "entry_point".to_string(), + ibc_adapter_contract_address: Addr::unchecked("ibc_transfer".to_string()), + sent_asset: Asset::Cw20(Cw20Coin { + address: "secret123".to_string(), + amount: 100u128.into(), + }), + ibc_info: IbcInfo { + source_channel: "source_channel".to_string(), + receiver: "receiver".to_string(), + fee: None, + memo: r#"{"pfm":"example_value","wasm":"example_contract"}"#.to_string(), + recover_address: "recover_address".to_string(), + }, + timeout_timestamp: 100, + expected_messages: vec![SubMsg { + id: 1, + msg: WasmMsg::Execute { + contract_addr: "secret123".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&snip20::ExecuteMsg::Send { + amount: 100u128.into(), + recipient: "ics20".to_string(), + recipient_code_hash: Some("code_hash".to_string()), + memo: Some(r#"{"ibc_callback":"ibc_transfer","pfm":"example_value","wasm":"example_contract"}"#.to_string()), + padding: None, + msg: Some(to_binary(&Ics20TransferMsg { + channel: "source_channel".to_string(), + remote_address: "receiver".to_string(), + timeout: Some(100), + })?), + })?, + funds: vec![], + }.into(), + gas_limit: None, + reply_on: Success, + }], + expected_error_string: "".to_string(), + }; + "Add Ibc Callback Key/Value Pair To Other Key/Value In Memo")] +#[test_case( + Params { + caller: "entry_point".to_string(), + ibc_adapter_contract_address: Addr::unchecked("ibc_transfer".to_string()), + sent_asset: Asset::Cw20(Cw20Coin { + address: "secret123".to_string(), + amount: 100u128.into(), + }), + ibc_info: IbcInfo { + source_channel: "source_channel".to_string(), + receiver: "receiver".to_string(), + fee: None, + memo: "{invalid}".to_string(), + recover_address: "recover_address".to_string(), + }, + timeout_timestamp: 100, + expected_messages: vec![], + expected_error_string: "Object key is not a string.".to_string(), + }; + "Non Empty String, Invalid Json Memo - Expect Error")] +#[test_case( + Params { + caller: "entry_point".to_string(), + ibc_adapter_contract_address: Addr::unchecked("ibc_transfer".to_string()), + sent_asset: Asset::Cw20(Cw20Coin { + address: "secret123".to_string(), + amount: 100u128.into(), + }), + ibc_info: IbcInfo { + source_channel: "source_channel".to_string(), + receiver: "receiver".to_string(), + fee: Some(IbcFee { + recv_fee: vec![ + Coin::new(100, "atom"), + ], + ack_fee: vec![], + timeout_fee: vec![], + }), + memo: "{}".to_string(), + recover_address: "recover_address".to_string(), + }, + timeout_timestamp: 100, + expected_messages: vec![], + expected_error_string: "IBC fees are not supported, vectors must be empty".to_string(), + }; + "IBC Fees Not Supported - Expect Error")] +#[test_case( + Params { + caller: "random".to_string(), + ibc_adapter_contract_address: Addr::unchecked("ibc_transfer".to_string()), + sent_asset: Asset::Cw20(Cw20Coin { + address: "secret123".to_string(), + amount: 100u128.into(), + }), + ibc_info: IbcInfo { + source_channel: "source_channel".to_string(), + receiver: "receiver".to_string(), + fee: None, + memo: "{}".to_string(), + recover_address: "recover_address".to_string(), + }, + timeout_timestamp: 100, + expected_messages: vec![], + expected_error_string: "Unauthorized".to_string(), + }; + "Unauthorized Caller - Expect Error")] +fn test_execute_ibc_transfer(params: Params) -> ContractResult<()> { + // Create mock dependencies + let mut deps = mock_dependencies(); + + // Create mock env + let mut env = mock_env(); + env.contract.address = params.ibc_adapter_contract_address.clone(); + env.contract.code_hash = "code_hash".to_string(); + + + // Store the entry point contract address + ENTRY_POINT_CONTRACT.save(deps.as_mut().storage, &ContractInfo { + address: Addr::unchecked("entry_point"), + code_hash: "code_hash".to_string(), + })?; + + ICS20_CONTRACT.save( + deps.as_mut().storage, + &ContractInfo { + address: Addr::unchecked("ics20"), + code_hash: "code_hash".to_string(), + }, + )?; + REGISTERED_TOKENS.save( + deps.as_mut().storage, + Addr::unchecked("secret123"), + &ContractInfo { + address: Addr::unchecked("secret123"), + code_hash: "code_hash".to_string(), + }, + )?; + VIEWING_KEY.save(deps.as_mut().storage, &"viewing_key".to_string())?; + + // Call execute_ibc_transfer with the given test parameters + let res = skip_go_secret_ibc_adapter_ibc_hooks::contract::execute( + deps.as_mut(), + env, + mock_info(&"secret123", &[]), + ExecuteMsg::Receive(Snip20ReceiveMsg { + sender: Addr::unchecked(params.caller.clone()), + amount: params.sent_asset.amount(), + from: Addr::unchecked(params.caller), + memo: None, + msg: Some( + to_binary(&Snip20HookMsg::IbcTransfer { + info: params.ibc_info.clone(), + timeout_timestamp: params.timeout_timestamp, + }) + .unwrap(), + ), + }), + ); + + // Assert the behavior is correct + match res { + Ok(res) => { + // Assert the test did not expect an error + assert!( + params.expected_error_string.is_empty(), + "expected test to error with {:?}, but it succeeded", + params.expected_error_string + ); + + // Assert the messages in the response are correct + assert_eq!(res.messages, params.expected_messages); + + // Load the in progress recover address from state and verify it is correct + let stored_in_progress_recover_address = + IN_PROGRESS_RECOVER_ADDRESS.load(&deps.storage)?; + + // Assert the in progress recover address is correct + assert_eq!( + stored_in_progress_recover_address, + params.ibc_info.recover_address + ); + + // Load the in progress channel id from state and verify it is correct + let stored_in_progress_channel_id = IN_PROGRESS_CHANNEL_ID.load(&deps.storage)?; + + // Assert the in progress channel id is correct + assert_eq!( + stored_in_progress_channel_id, + params.ibc_info.source_channel + ); + } + Err(err) => { + // Assert the test expected an error + assert!( + !params.expected_error_string.is_empty(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err.to_string(), params.expected_error_string); + } + } + + Ok(()) +} diff --git a/contracts/adapters/ibc/secret-ibc-hooks/tests/test_reply.rs b/contracts/adapters/ibc/secret-ibc-hooks/tests/test_reply.rs new file mode 100644 index 00000000..f715e259 --- /dev/null +++ b/contracts/adapters/ibc/secret-ibc-hooks/tests/test_reply.rs @@ -0,0 +1,258 @@ +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env}, + Reply, StdError, SubMsgResponse, SubMsgResult, +}; +use ibc_proto::ibc::applications::transfer::v1::MsgTransferResponse; +use prost::Message; +use skip_go_secret_ibc_adapter_ibc_hooks::{ + error::ContractResult, + state::{ACK_ID_TO_RECOVER_ADDRESS, IN_PROGRESS_CHANNEL_ID, IN_PROGRESS_RECOVER_ADDRESS}, +}; +use test_case::test_case; + +/* +Test Cases: + +Expect Success + - Happy Path (tests the in progress ibc transfer is removed from storage and the ack id to in progress ibc transfer map entry is correct) + +Expect Error + - Missing Sub Msg Response Data + - Invalid Sub Msg Response Data To Convert To MsgTransferResponse + - No In Progress Recover Address To Load + - No In Progress Channel ID To Load + - Ack ID Already Exists + +Expect Panic + - SubMsgResult Error + - Should panic because the sub msg is set to reply only on success, so should never happen + unless the wasm module worked unexpectedly + - SubMsg Incorrect Reply ID + - Should panic because the reply id is set to a constant, so should never happen unless + the wasm module worked unexpectedly + */ + +// Define test parameters +struct Params { + channel_id: String, + sequence_id: u64, + reply: Reply, + pre_reply_in_progress_recover_address: Option, + pre_reply_in_progress_channel_id: Option, + store_ack_id_to_recover_address: bool, + expected_error_string: String, +} + +// Test reply +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 5, + reply: Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(MsgTransferResponse {sequence: 5}.encode_to_vec().as_slice().into()), + }), + }, + pre_reply_in_progress_recover_address: Some("recover_address".to_string()), + pre_reply_in_progress_channel_id: Some("channel_id".to_string()), + store_ack_id_to_recover_address: false, + expected_error_string: "".to_string(), + }; + "Happy Path")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + reply: Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: None, + }), + }, + pre_reply_in_progress_recover_address: None, + pre_reply_in_progress_channel_id: None, + store_ack_id_to_recover_address: false, + expected_error_string: "SubMsgResponse does not contain data".to_string(), + }; + "Missing Sub Msg Response Data - Expect Error")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + reply: Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(b"invalid".into()), + }), + }, + pre_reply_in_progress_recover_address: None, + pre_reply_in_progress_channel_id: None, + store_ack_id_to_recover_address: false, + expected_error_string: "failed to decode Protobuf message: buffer underflow".to_string(), + }; + "Invalid Sub Msg Response Data To Convert To MsgTransferResponse - Expect Error")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + reply: Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(MsgTransferResponse {sequence: 5}.encode_to_vec().as_slice().into()), + }), + }, + pre_reply_in_progress_recover_address: None, + pre_reply_in_progress_channel_id: Some("channel_id".to_string()), + store_ack_id_to_recover_address: false, + expected_error_string: "alloc::string::String not found".to_string(), + }; + "No In Progress Recover Address To Load - Expect Error")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + reply: Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(MsgTransferResponse {sequence: 5}.encode_to_vec().as_slice().into()), + }), + }, + pre_reply_in_progress_recover_address: Some("recover_address".to_string()), + pre_reply_in_progress_channel_id: None, + store_ack_id_to_recover_address: false, + expected_error_string: "alloc::string::String not found".to_string(), + }; + "No In Progress Channel ID To Load - Expect Error")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 5, + reply: Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(MsgTransferResponse {sequence: 5}.encode_to_vec().as_slice().into()), + }), + }, + pre_reply_in_progress_recover_address: Some("recover_address".to_string()), + pre_reply_in_progress_channel_id: Some("channel_id".to_string()), + store_ack_id_to_recover_address: true, + expected_error_string: "ACK ID already exists for channel ID channel_id and sequence ID 5".to_string(), + }; + "Ack ID Already Exists - Expect Error")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + reply: Reply { + id: 2, + result: SubMsgResult::Err("".to_string()), + }, + pre_reply_in_progress_recover_address: Some("recover_address".to_string()), + pre_reply_in_progress_channel_id: Some("channel_id".to_string()), + store_ack_id_to_recover_address: false, + expected_error_string: "".to_string(), + } => panics "internal error: entered unreachable code"; + "SubMsg Incorrect Reply ID - Expect Panic")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + reply: Reply { + id: 1, + result: SubMsgResult::Err("".to_string()), + }, + pre_reply_in_progress_recover_address: Some("recover_address".to_string()), + pre_reply_in_progress_channel_id: Some("channel_id".to_string()), + expected_error_string: "".to_string(), + store_ack_id_to_recover_address: false, + } => panics "internal error: entered unreachable code"; + "SubMsgResult Error - Expect Panic")] +fn test_reply(params: Params) -> ContractResult<()> { + // Create mock dependencies + let mut deps = mock_dependencies(); + + // Create mock env + let env = mock_env(); + + // Store the in progress recover address to state if it exists + if let Some(in_progress_recover_address) = params.pre_reply_in_progress_recover_address.clone() + { + IN_PROGRESS_RECOVER_ADDRESS.save(deps.as_mut().storage, &in_progress_recover_address)?; + } + + // Store the in progress channel id to state if it exists + if let Some(in_progress_channel_id) = params.pre_reply_in_progress_channel_id.clone() { + IN_PROGRESS_CHANNEL_ID.save(deps.as_mut().storage, &in_progress_channel_id)?; + } + + // If the test expects the ack id to in progress ibc transfer map entry to be stored, + // store it to state + if params.store_ack_id_to_recover_address { + ACK_ID_TO_RECOVER_ADDRESS.save( + deps.as_mut().storage, + (¶ms.channel_id, params.sequence_id), + ¶ms + .pre_reply_in_progress_recover_address + .clone() + .unwrap(), + )?; + } + + // Call reply with the given test parameters + let res = + skip_go_secret_ibc_adapter_ibc_hooks::contract::reply(deps.as_mut(), env, params.reply); + + // Assert the behavior is correct + match res { + Ok(_) => { + // Assert the test did not expect an error + assert!( + params.expected_error_string.is_empty(), + "expected test to error with {:?}, but it succeeded", + params.expected_error_string + ); + + // Verify the in progress ibc transfer was removed from storage + match IN_PROGRESS_RECOVER_ADDRESS.load(&deps.storage) { + Ok(in_progress_ibc_transfer) => { + panic!( + "expected in progress ibc transfer to be removed: {:?}", + in_progress_ibc_transfer + ) + } + Err(err) => assert!( + matches!(err, StdError::NotFound { .. }), + "unexpected error: {:?}", + err + ), + }; + + // Verify the stored ack id to in progress ibc transfer map entry is correct + assert_eq!( + ACK_ID_TO_RECOVER_ADDRESS + .load(&deps.storage, (¶ms.channel_id, params.sequence_id))?, + params.pre_reply_in_progress_recover_address.unwrap() + ); + } + Err(err) => { + // Assert the test expected an error + assert!( + !params.expected_error_string.is_empty(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err.to_string(), params.expected_error_string); + } + } + + Ok(()) +} diff --git a/contracts/adapters/ibc/secret-ibc-hooks/tests/test_sudo.rs b/contracts/adapters/ibc/secret-ibc-hooks/tests/test_sudo.rs new file mode 100644 index 00000000..deb2bea3 --- /dev/null +++ b/contracts/adapters/ibc/secret-ibc-hooks/tests/test_sudo.rs @@ -0,0 +1,205 @@ +use cosmwasm_std::{ + testing::{mock_dependencies_with_balances, mock_env}, + Addr, BankMsg, Coin, + ReplyOn::Never, + StdError, SubMsg, +}; +use secret_skip::{ibc::IbcLifecycleComplete, sudo::OsmosisSudoMsg as SudoMsg}; +use skip_go_secret_ibc_adapter_ibc_hooks::{ + error::ContractResult, state::ACK_ID_TO_RECOVER_ADDRESS, +}; +use test_case::test_case; + +/* +Test Cases: + +Expect Success + - Sudo Response - Happy Path Response + - Sudo Timeout - Send Failed Ibc Coin To Recover Address + - Sudo Error - Send Failed Ibc Coin To Recover Address + +Expect Error + - No In Progress Recover Address Mapped To Sudo Ack ID - Expect Error + - No Contract Balance To Refund - Expect Error + + */ + +// Define test parameters +struct Params { + contract_balance: Vec, + channel_id: String, + sequence_id: u64, + sudo_msg: SudoMsg, + stored_in_progress_recover_address: Option, + expected_messages: Vec, + expected_error_string: String, +} + +// Test sudo +#[test_case( + Params { + contract_balance: vec![], + channel_id: "channel_id".to_string(), + sequence_id: 1, + sudo_msg: SudoMsg::IbcLifecycleComplete(IbcLifecycleComplete::IbcAck{ + channel: "channel_id".to_string(), + sequence: 1, + ack: "".to_string(), + success: true, + }), + stored_in_progress_recover_address: Some("recover_address".to_string()), + expected_messages: vec![], + expected_error_string: "".to_string(), + }; + "Sudo Response - Happy Path")] +#[test_case( + Params { + contract_balance: vec![Coin::new(100, "uosmo")], + channel_id: "channel_id".to_string(), + sequence_id: 1, + sudo_msg: SudoMsg::IbcLifecycleComplete(IbcLifecycleComplete::IbcTimeout{ + channel: "channel_id".to_string(), + sequence: 1, + }), + stored_in_progress_recover_address: Some("recover_address".to_string()), + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "recover_address".to_string(), + amount: vec![Coin::new(100, "uosmo")], + }.into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error_string: "".to_string(), + }; + "Sudo Timeout - Send Failed Ibc Coin")] +#[test_case( + Params { + contract_balance: vec![Coin::new(100, "uosmo")], + channel_id: "channel_id".to_string(), + sequence_id: 1, + sudo_msg: SudoMsg::IbcLifecycleComplete(IbcLifecycleComplete::IbcAck{ + channel: "channel_id".to_string(), + sequence: 1, + ack: "".to_string(), + success: false, + }), + stored_in_progress_recover_address: Some("recover_address".to_string()), + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "recover_address".to_string(), + amount: vec![Coin::new(100, "uosmo")], + }.into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error_string: "".to_string(), + }; + "Sudo Error - Send Failed Ibc Coin To Recover Address")] +#[test_case( + Params { + contract_balance: vec![Coin::new(100, "uosmo")], + channel_id: "channel_id".to_string(), + sequence_id: 1, + sudo_msg: SudoMsg::IbcLifecycleComplete(IbcLifecycleComplete::IbcAck{ + channel: "channel_id".to_string(), + sequence: 1, + ack: "".to_string(), + success: false, + }), + stored_in_progress_recover_address: None, + expected_messages: vec![], + expected_error_string: "alloc::string::String not found".to_string(), + }; + "No In Progress Ibc Transfer Mapped To Sudo Ack ID - Expect Error")] +#[test_case( + Params { + contract_balance: vec![], + channel_id: "channel_id".to_string(), + sequence_id: 1, + sudo_msg: SudoMsg::IbcLifecycleComplete(IbcLifecycleComplete::IbcAck{ + channel: "channel_id".to_string(), + sequence: 1, + ack: "".to_string(), + success: false, + }), + stored_in_progress_recover_address: Some("recover_address".to_string()), + expected_messages: vec![], + expected_error_string: "Failed to receive ibc funds to refund the user".to_string(), + }; + "No Contract Balance To Refund - Expect Error")] +fn test_sudo(params: Params) -> ContractResult<()> { + // Convert params contract balance to a slice + let contract_balance: &[Coin] = ¶ms.contract_balance; + + // Create mock dependencies + let mut deps = mock_dependencies_with_balances(&[("ibc_transfer_adapter", contract_balance)]); + + // Create mock env + let mut env = mock_env(); + env.contract.address = Addr::unchecked("ibc_transfer_adapter"); + + // Store the in progress recover address to state if it exists + if let Some(in_progress_recover_address) = params.stored_in_progress_recover_address.clone() { + ACK_ID_TO_RECOVER_ADDRESS.save( + deps.as_mut().storage, + (¶ms.channel_id, params.sequence_id), + &in_progress_recover_address, + )?; + } + + // Call sudo with the given test parameters + let res = + skip_go_secret_ibc_adapter_ibc_hooks::contract::sudo(deps.as_mut(), env, params.sudo_msg); + + // Assert the behavior is correct + match res { + Ok(res) => { + // Assert the test did not expect an error + assert!( + params.expected_error_string.is_empty(), + "expected test to error with {:?}, but it succeeded", + params.expected_error_string + ); + + // Verify the in progress recover address was removed from storage + match ACK_ID_TO_RECOVER_ADDRESS + .load(&deps.storage, (¶ms.channel_id, params.sequence_id)) + { + Ok(in_progress_recover_address) => { + panic!( + "expected in progress recover address to be removed: {:?}", + in_progress_recover_address + ) + } + Err(err) => assert!( + matches!(err, StdError::NotFound { .. }), + "unexpected error: {:?}", + err + ), + }; + + // Verify the messages in the response are correct + assert_eq!(res.messages, params.expected_messages); + } + Err(err) => { + // Assert the test expected an error + assert!( + !params.expected_error_string.is_empty(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err.to_string(), params.expected_error_string); + } + } + + Ok(()) +} diff --git a/contracts/adapters/swap/shade-protocol/Cargo.toml b/contracts/adapters/swap/shade-protocol/Cargo.toml new file mode 100644 index 00000000..a3ca6b2b --- /dev/null +++ b/contracts/adapters/swap/shade-protocol/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "skip-go-swap-adapter-shade-protocol" +version = { workspace = true } +rust-version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +documentation = { workspace = true } +keywords = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +#cw2 = { workspace = true } +#cw20 = { workspace = true } +#cw-utils = { workspace = true } +thiserror = { workspace = true } +secret-skip = { workspace = true } + +cosmwasm-std = { package = "secret-cosmwasm-std", version = "1.1.11"} +cosmwasm-schema = "1.4.0" +secret-toolkit = { git = "https://github.com/scrtlabs/secret-toolkit", tag = "v0.10.0" } +secret-storage-plus = { git = "https://github.com/securesecrets/secret-plus-utils", tag = "v0.1.1", features = [] } +serde = "1.0.114" +schemars = "0.8.1" + +[dev-dependencies] +test-case = { workspace = true } diff --git a/contracts/adapters/swap/shade-protocol/README.md b/contracts/adapters/swap/shade-protocol/README.md new file mode 100644 index 00000000..0354b959 --- /dev/null +++ b/contracts/adapters/swap/shade-protocol/README.md @@ -0,0 +1,123 @@ +# Neutron Astroport Swap Adapter Contract + +The Neutron Astroport swap adapter contract is responsible for: +1. Taking the standardized entry point swap operations message format and converting it to Astroport pool swaps message format. +2. Swapping by dispatching swaps to Astroport pool contracts. +3. Providing query methods that can be called by the entry point contract (generally, to any external actor) to simulate multi-hop swaps that either specify an exact amount in (estimating how much would be received from the swap) or an exact amount out (estimating how much is required to get the specified amount out). + +Note: Swap adapter contracts expect to be called by an entry point contract that provides basic validation and minimum amount out safety guarantees for the caller. There are no slippage guarantees provided by swap adapter contracts. + +WARNING: Do not send funds directly to the contract without calling one of its functions. Funds sent directly to the contract do not trigger any contract logic that performs validation / safety checks (as the Cosmos SDK handles direct fund transfers in the `Bank` module and not the `Wasm` module). There are no explicit recovery mechanisms for accidentally sent funds. + +## InstantiateMsg + +Instantiates a new Neutron Astroport swap adapter contract using the Entrypoint contract address provided in the instantiation message. + +``` json +{ + "entry_point_contract_address": "neutron..." +} +``` + +## ExecuteMsg + +### `swap` + +Swaps the coin sent using the operations provided. + +``` json +{ + "swap": { + "operations": [ + { + "pool": "neutron...", + "denom_in": "uatom", + "denom_out": "untrn" + }, + { + "pool": "neutron...", + "denom_in": "untrn", + "denom_out": "uosmo" + } + ] + } +} +``` + +### `transfer_funds_back` + +Transfers all contract funds to the address provided, called by the swap adapter contract to send back the entry point contract the assets received from swapping. + +Note: This function can be called by anyone as the contract is assumed to have no balance before/after it's called by the entry point contract. Do not send funds directly to this contract without calling a function. + +``` json +{ + "transfer_funds_back": { + "caller": "neutron..." + } +} +``` + +## QueryMsg + +### `simulate_swap_exact_coin_out` + +Returns the coin in required to receive the `coin_out` specified in the call (swapped through the `swap_operatons` provided) + +Query: +``` json +{ + "simulate_swap_exact_coin_out": { + "coin_out": { + "denom": "untrn", + "amount": "200000" + }, + "swap_operations": [ + { + "pool": "neutron...", + "denom_in": "uatom", + "denom_out": "untrn" + } + ] + } +} +``` + +Response: +``` json +{ + "denom": "uatom", + "amount": "100" +} +``` + +### `simulate_swap_exact_coin_in` + +Returns the coin out that would be received from swapping the `coin_in` specified in the call (swapped through the `swap_operatons` provided) + +Query: +``` json +{ + "simulate_swap_exact_coin_in": { + "coin_in": { + "denom": "uatom", + "amount": "100" + }, + "swap_operations": [ + { + "pool": "neutron...", + "denom_in": "uatom", + "denom_out": "untrn" + } + ] + } +} +``` + +Response: +``` json +{ + "denom": "untrn", + "amount": "100000" +} +``` \ No newline at end of file diff --git a/contracts/adapters/swap/shade-protocol/src/bin/shade-protocol-schema.rs b/contracts/adapters/swap/shade-protocol/src/bin/shade-protocol-schema.rs new file mode 100644 index 00000000..e5cc9e2f --- /dev/null +++ b/contracts/adapters/swap/shade-protocol/src/bin/shade-protocol-schema.rs @@ -0,0 +1,10 @@ +use cosmwasm_schema::write_api; +use secret_skip::swap::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg + } +} diff --git a/contracts/adapters/swap/shade-protocol/src/contract.rs b/contracts/adapters/swap/shade-protocol/src/contract.rs new file mode 100644 index 00000000..d5b76e7c --- /dev/null +++ b/contracts/adapters/swap/shade-protocol/src/contract.rs @@ -0,0 +1,435 @@ +use crate::{ + error::{ContractError, ContractResult}, + state::{ + ENTRY_POINT_CONTRACT, REGISTERED_TOKENS, SHADE_POOL_CODE_HASH, SHADE_ROUTER_CONTRACT, + VIEWING_KEY, + }, +}; +use cosmwasm_std::{ + entry_point, from_binary, to_binary, Addr, Binary, ContractInfo, Deps, DepsMut, Env, + MessageInfo, Response, Uint128, WasmMsg, +}; +use secret_skip::{asset::Asset, cw20::Cw20Coin, snip20::Snip20ReceiveMsg, swap::SwapOperation}; +// use cw2::set_contract_version; +use secret_toolkit::snip20; + +use crate::{ + msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, Snip20HookMsg}, + shade_swap_router_msg as shade_router, +}; + +// Contract name and version used for migration. +/* +const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +*/ + +/////////////// +/// MIGRATE /// +/////////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> ContractResult { + // Set contract version + // set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + // Validate entry point contract address + let checked_entry_point_contract = ContractInfo { + address: deps + .api + .addr_validate(&msg.entry_point_contract.address.to_string())?, + code_hash: msg.entry_point_contract.code_hash, + }; + + ENTRY_POINT_CONTRACT.save(deps.storage, &checked_entry_point_contract)?; + + let checked_shade_router_contract = ContractInfo { + address: deps + .api + .addr_validate(&msg.shade_router_contract.address.to_string())?, + code_hash: msg.shade_router_contract.code_hash, + }; + + SHADE_ROUTER_CONTRACT.save(deps.storage, &checked_shade_router_contract)?; + + VIEWING_KEY.save(deps.storage, &msg.viewing_key)?; + + Ok(Response::new() + .add_attribute("action", "migrate") + .add_attribute( + "entry_point_contract_address", + checked_entry_point_contract.address.to_string(), + ) + .add_attribute( + "shade_router_contract_address", + checked_shade_router_contract.address.to_string(), + )) +} + +/////////////////// +/// INSTANTIATE /// +/////////////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> ContractResult { + // Set contract version + // set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + // Validate entry point contract + let checked_entry_point_contract = ContractInfo { + address: deps + .api + .addr_validate(&msg.entry_point_contract.address.to_string())?, + code_hash: msg.entry_point_contract.code_hash, + }; + + ENTRY_POINT_CONTRACT.save(deps.storage, &checked_entry_point_contract)?; + + let checked_shade_router_contract = ContractInfo { + address: deps + .api + .addr_validate(&msg.shade_router_contract.address.to_string())?, + code_hash: msg.shade_router_contract.code_hash, + }; + + SHADE_ROUTER_CONTRACT.save(deps.storage, &checked_shade_router_contract)?; + + VIEWING_KEY.save(deps.storage, &msg.viewing_key)?; + + Ok(Response::new() + .add_attribute("action", "instantiate") + .add_attribute( + "entry_point_contract_address", + checked_entry_point_contract.address.to_string(), + ) + .add_attribute( + "shade_router_contract_address", + msg.shade_router_contract.address, + )) +} + +/////////////// +/// RECEIVE /// +/////////////// + +// Receive is the main entry point for the contract to +// receive cw20 tokens and execute the swap +pub fn receive_snip20( + deps: DepsMut, + env: Env, + mut info: MessageInfo, + snip20_msg: Snip20ReceiveMsg, +) -> ContractResult { + // Set the sender to the originating address that triggered the cw20 send call + // This is later validated / enforced to be the entry point contract address + info.sender = deps.api.addr_validate(&snip20_msg.sender.to_string())?; + + match snip20_msg.msg { + Some(msg) => match from_binary(&msg)? { + Snip20HookMsg::Swap { operations } => { + execute_swap(deps, env, info, operations, snip20_msg.amount) + } + }, + None => Ok(Response::default()), + } +} + +/////////////// +/// EXECUTE /// +/////////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> ContractResult { + match msg { + ExecuteMsg::Receive(snip20_msg) => receive_snip20(deps, env, info, snip20_msg), + ExecuteMsg::TransferFundsBack { + swapper, + return_denom, + } => Ok(execute_transfer_funds_back( + deps, + env, + info, + swapper, + return_denom, + )?), + // Tokens must be registered before they can be swapped + ExecuteMsg::RegisterTokens { contracts } => register_tokens(deps, env, contracts), + } +} + +fn execute_swap( + deps: DepsMut, + env: Env, + info: MessageInfo, + operations: Vec, + input_amount: Uint128, +) -> ContractResult { + // Get entry point contract from storage + let entry_point_contract = ENTRY_POINT_CONTRACT.load(deps.storage)?; + + // Enforce the caller is the entry point contract + if info.sender != entry_point_contract.address { + return Err(ContractError::Unauthorized); + } + + // Get pool code hash from storage + let shade_pool_code_hash = SHADE_POOL_CODE_HASH.load(deps.storage)?; + + // Build shade router swap message + let mut path = vec![]; + for operation in &operations { + path.push(shade_router::Hop { + addr: operation.pool.to_string(), + code_hash: shade_pool_code_hash.clone(), + }); + } + + // Input denom will be sent to router + let input_denom = match operations.first() { + Some(first_op) => first_op.denom_in.clone(), + None => return Err(ContractError::SwapOperationsEmpty), + }; + // Used for transfer funds back + let return_denom = match operations.last() { + Some(last_op) => last_op.denom_out.clone(), + None => return Err(ContractError::SwapOperationsEmpty), + }; + + let input_denom_contract = REGISTERED_TOKENS.load( + deps.storage, + deps.api.addr_validate(&input_denom.to_string())?, + )?; + + // Get shade router contract from storage + let shade_router_contract = SHADE_ROUTER_CONTRACT.load(deps.storage)?; + + // Create a response object to return + Ok(Response::new() + .add_attribute("action", "execute_swap") + .add_attribute("action", "dispatch_swaps_and_transfer_back") + // Swap router execution + .add_message(snip20::send_msg_with_code_hash( + shade_router_contract.address.to_string(), + Some(shade_router_contract.code_hash), + input_amount, + Some(to_binary(&shade_router::InvokeMsg::SwapTokensForExact { + path, + expected_return: None, + recipient: None, + })?), + None, + None, + 0, + input_denom_contract.code_hash, + input_denom_contract.address.to_string(), + )?) + // TransferFundsBack message to self + .add_message(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + code_hash: env.contract.code_hash, + msg: to_binary(&ExecuteMsg::TransferFundsBack { + swapper: entry_point_contract.address, + return_denom, + })?, + funds: vec![], + })) +} + +fn register_tokens( + deps: DepsMut, + env: Env, + contracts: Vec, +) -> ContractResult { + let mut response = Response::new(); + + let viewing_key = VIEWING_KEY.load(deps.storage)?; + + for contract in contracts.iter() { + // Add to storage for later use of code hash + REGISTERED_TOKENS.save(deps.storage, contract.address.clone(), contract)?; + // register receive, set viewing key, & add attribute + response = response + .add_attribute("register_token", contract.address.clone()) + .add_messages(vec![ + snip20::set_viewing_key_msg( + viewing_key.clone(), + None, + 0, + contract.code_hash.clone(), + contract.address.to_string(), + )?, + snip20::register_receive_msg( + env.contract.code_hash.clone(), + None, + 0, + contract.code_hash.clone(), + contract.address.to_string(), + )?, + ]); + } + + Ok(response) +} + +pub fn execute_transfer_funds_back( + deps: DepsMut, + env: Env, + info: MessageInfo, + swapper: Addr, + return_denom: String, +) -> ContractResult { + // Ensure the caller is the contract itself + if info.sender != env.contract.address { + return Err(ContractError::Unauthorized); + } + + // Validate return_denom + let return_denom = match deps.api.addr_validate(&return_denom) { + Ok(addr) => addr, + Err(_) => return Err(ContractError::InvalidSnip20Coin), + }; + + // Load token contract + let token_contract = match REGISTERED_TOKENS.load(deps.storage, return_denom) { + Ok(contract) => contract, + Err(_) => return Err(ContractError::InvalidSnip20Coin), + }; + + let viewing_key = VIEWING_KEY.load(deps.storage)?; + + let balance = match snip20::balance_query( + deps.querier, + env.contract.address.to_string(), + viewing_key, + 0, + token_contract.code_hash.clone(), + token_contract.address.to_string(), + ) { + Ok(balance) => balance, + Err(e) => return Err(ContractError::Std(e)), + }; + + // let entry_point_contract = ENTRY_POINT_CONTRACT.load(deps.storage)?; + + let send_msg = match snip20::send_msg( + swapper.to_string(), + balance.amount, + None, + None, + None, + 0, + token_contract.code_hash.clone(), + token_contract.address.to_string(), + ) { + Ok(msg) => msg, + Err(e) => return Err(ContractError::Std(e)), + }; + + Ok(Response::new() + .add_message(send_msg) + .add_attribute("action", "dispatch_transfer_funds_back_bank_send")) +} + +///////////// +/// QUERY /// +///////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> ContractResult { + match msg { + QueryMsg::SimulateSwapExactAssetIn { + asset_in, + swap_operations, + } => to_binary(&query_simulate_swap_exact_asset_in( + deps, + asset_in, + swap_operations, + )?), + } + .map_err(From::from) +} + +// Queries the astroport pool contracts to simulate a swap exact amount in +fn query_simulate_swap_exact_asset_in( + deps: Deps, + asset_in: Asset, + swap_operations: Vec, +) -> ContractResult { + // Error if swap operations is empty + let Some(first_op) = swap_operations.first() else { + return Err(ContractError::SwapOperationsEmpty); + }; + + // Ensure asset_in's denom is the same as the first swap operation's denom in + if asset_in.denom() != first_op.denom_in { + return Err(ContractError::CoinInDenomMismatch); + } + + let asset_out = simulate_swap_exact_asset_in(deps, asset_in, swap_operations)?; + + // Return the asset out + Ok(asset_out) +} + +// Simulates a swap exact amount in request, returning the asset out and optionally the reverse simulation responses +fn simulate_swap_exact_asset_in( + deps: Deps, + asset_in: Asset, + swap_operations: Vec, +) -> ContractResult { + // Load state from storage + let shade_pool_code_hash = SHADE_POOL_CODE_HASH.load(deps.storage)?; + let shade_router_contract = SHADE_ROUTER_CONTRACT.load(deps.storage)?; + + // Get contract data for asset_in + let asset_in_contract = + REGISTERED_TOKENS.load(deps.storage, deps.api.addr_validate(asset_in.denom())?)?; + + let denom_out = match swap_operations.last() { + Some(last_op) => last_op.denom_out.clone(), + None => return Err(ContractError::SwapOperationsEmpty), + }; + + let mut path = vec![]; + for operation in swap_operations.iter() { + path.push(shade_router::Hop { + addr: operation.pool.to_string(), + code_hash: shade_pool_code_hash.clone(), + }); + } + + let sim_response: shade_router::QueryMsgResponse = deps.querier.query_wasm_smart( + &shade_router_contract.address, + &shade_router_contract.code_hash, + &shade_router::QueryMsg::SwapSimulation { + offer: shade_router::TokenAmount { + token: shade_router::TokenType::CustomToken { + contract_addr: deps.api.addr_validate(asset_in.denom())?, + token_code_hash: asset_in_contract.code_hash, + }, + amount: Uint128::new(asset_in.amount().u128()), + }, + path, + exclude_fee: None, + }, + )?; + + let amount_out = match sim_response { + shade_router::QueryMsgResponse::SwapSimulation { result, .. } => result.return_amount, + }; + + Ok(Asset::Cw20(Cw20Coin { + address: denom_out.to_string(), + amount: amount_out.u128().into(), + })) +} diff --git a/contracts/adapters/swap/shade-protocol/src/error.rs b/contracts/adapters/swap/shade-protocol/src/error.rs new file mode 100644 index 00000000..34ffab5a --- /dev/null +++ b/contracts/adapters/swap/shade-protocol/src/error.rs @@ -0,0 +1,42 @@ +use cosmwasm_std::{OverflowError, StdError}; +use secret_skip::error::SkipError; +use thiserror::Error; + +pub type ContractResult = core::result::Result; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error(transparent)] + Overflow(#[from] OverflowError), + + #[error(transparent)] + Skip(#[from] SkipError), + + /* + #[error(transparent)] + Payment(#[from] cw_utils::PaymentError), + */ + #[error("Unauthorized")] + Unauthorized, + + #[error("swap_operations cannot be empty")] + SwapOperationsEmpty, + + #[error("coin_in denom must match the first swap operation's denom in")] + CoinInDenomMismatch, + + #[error("coin_out denom must match the last swap operation's denom out")] + CoinOutDenomMismatch, + + #[error("Operation exceeds max spread limit")] + MaxSpreadAssertion, + + #[error("Contract has no balance of offer asset")] + NoOfferAssetAmount, + + #[error("Snip20 Coin Sent To Contract Does Not Match Asset")] + InvalidSnip20Coin, +} diff --git a/contracts/adapters/swap/shade-protocol/src/lib.rs b/contracts/adapters/swap/shade-protocol/src/lib.rs new file mode 100644 index 00000000..be2d81d2 --- /dev/null +++ b/contracts/adapters/swap/shade-protocol/src/lib.rs @@ -0,0 +1,5 @@ +pub mod contract; +pub mod error; +pub mod msg; +pub mod shade_swap_router_msg; +pub mod state; diff --git a/contracts/adapters/swap/shade-protocol/src/msg.rs b/contracts/adapters/swap/shade-protocol/src/msg.rs new file mode 100644 index 00000000..c945bd07 --- /dev/null +++ b/contracts/adapters/swap/shade-protocol/src/msg.rs @@ -0,0 +1,42 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, ContractInfo}; +use secret_skip::{asset::Asset, snip20::Snip20ReceiveMsg, swap::SwapOperation}; + +#[cw_serde] +pub struct InstantiateMsg { + pub entry_point_contract: ContractInfo, + pub shade_router_contract: ContractInfo, + pub shade_pool_code_hash: String, + pub viewing_key: String, +} + +#[cw_serde] +pub struct MigrateMsg { + pub entry_point_contract: ContractInfo, + pub shade_router_contract: ContractInfo, + pub shade_pool_code_hash: String, + pub viewing_key: String, +} + +#[cw_serde] +pub enum ExecuteMsg { + Receive(Snip20ReceiveMsg), + TransferFundsBack { swapper: Addr, return_denom: String }, + RegisterTokens { contracts: Vec }, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + // SimulateSwapExactAssetIn returns the asset out received from the specified asset in + #[returns(Asset)] + SimulateSwapExactAssetIn { + asset_in: Asset, + swap_operations: Vec, + }, +} + +#[cw_serde] +pub enum Snip20HookMsg { + Swap { operations: Vec }, +} diff --git a/contracts/adapters/swap/shade-protocol/src/shade_swap_router_msg.rs b/contracts/adapters/swap/shade-protocol/src/shade_swap_router_msg.rs new file mode 100644 index 00000000..b0efe845 --- /dev/null +++ b/contracts/adapters/swap/shade-protocol/src/shade_swap_router_msg.rs @@ -0,0 +1,103 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Uint128}; +use schemars::JsonSchema; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +#[cw_serde] +pub struct TokenAmount { + pub token: TokenType, + pub amount: Uint128, +} + +#[cw_serde] +pub enum TokenType { + CustomToken { + contract_addr: Addr, + token_code_hash: String, + }, + NativeToken { + denom: String, + }, +} + +#[cw_serde] +pub struct StableTokenData { + pub oracle_key: String, + pub decimals: u8, +} + +#[cw_serde] +pub struct StableTokenType { + pub token: TokenType, + pub stable_token_data: StableTokenData, +} + +#[derive(Clone, Debug, JsonSchema)] +pub struct TokenPair(pub TokenType, pub TokenType, pub bool); + +impl Serialize for TokenPair { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + (self.0.clone(), self.1.clone(), self.2.clone()).serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for TokenPair { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Deserialize::deserialize(deserializer) + .map(|(token_0, token_1, is_stable)| TokenPair(token_0, token_1, is_stable)) + } +} + +#[cw_serde] +pub enum ExecuteMsgResponse { + SwapResult { + amount_in: Uint128, + amount_out: Uint128, + }, +} + +#[cw_serde] +pub enum InvokeMsg { + SwapTokensForExact { + path: Vec, + expected_return: Option, + recipient: Option, + }, +} + +#[cw_serde] +pub struct Hop { + pub addr: String, + pub code_hash: String, +} + +#[cw_serde] +pub struct SwapResult { + pub return_amount: Uint128, +} + +#[cw_serde] +pub enum QueryMsg { + SwapSimulation { + offer: TokenAmount, + path: Vec, + exclude_fee: Option, + }, +} + +#[cw_serde] +pub enum QueryMsgResponse { + SwapSimulation { + total_fee_amount: Uint128, + lp_fee_amount: Uint128, + shade_dao_fee_amount: Uint128, + result: SwapResult, + price: String, + }, +} diff --git a/contracts/adapters/swap/shade-protocol/src/state.rs b/contracts/adapters/swap/shade-protocol/src/state.rs new file mode 100644 index 00000000..f968ae87 --- /dev/null +++ b/contracts/adapters/swap/shade-protocol/src/state.rs @@ -0,0 +1,10 @@ +use cosmwasm_std::{Addr, ContractInfo}; +use secret_storage_plus::Map; +use secret_toolkit::storage::Item; + +pub const ENTRY_POINT_CONTRACT: Item = Item::new(b"entry_point_contract"); +pub const SHADE_ROUTER_CONTRACT: Item = Item::new(b"shade_router_contract"); + +pub const SHADE_POOL_CODE_HASH: Item = Item::new(b"shade_pool_code_hash"); +pub const VIEWING_KEY: Item = Item::new(b"viewing_key"); +pub const REGISTERED_TOKENS: Map = Map::new("registered_tokens"); diff --git a/contracts/adapters/swap/shade-protocol/tests/test_execute_receive.rs b/contracts/adapters/swap/shade-protocol/tests/test_execute_receive.rs new file mode 100644 index 00000000..776eea0c --- /dev/null +++ b/contracts/adapters/swap/shade-protocol/tests/test_execute_receive.rs @@ -0,0 +1,221 @@ +use core::panic; +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env, mock_info}, + to_binary, Addr, Coin, ContractInfo, ContractResult as SystemContractResult, QuerierResult, + ReplyOn::Never, + SubMsg, SystemResult, Uint128, WasmMsg, WasmQuery, +}; +use secret_skip::{asset::Asset, cw20::Cw20Coin, snip20, swap::SwapOperation}; +use skip_go_swap_adapter_shade_protocol::{ + error::{ContractError, ContractResult}, + msg::{ExecuteMsg, Snip20HookMsg}, + shade_swap_router_msg as shade_router, + state::{ENTRY_POINT_CONTRACT, REGISTERED_TOKENS, SHADE_POOL_CODE_HASH, SHADE_ROUTER_CONTRACT}, +}; +use test_case::test_case; + +/* +Test Cases: + +Expect Success + - One Swap Operation - Cw20 In + - One Swap Operation - Cw20 In And Out + +Expect Error + - Coin sent with cw20 + + */ + +// Define test parameters +struct Params { + caller: String, + info_funds: Vec, + sent_asset: Asset, + swap_operations: Vec, + expected_messages: Vec, + expected_error: Option, +} + +// Test execute_swap +#[test_case( + Params { + caller: "entry_point".to_string(), + info_funds: vec![], + sent_asset: Asset::Cw20(Cw20Coin { + address: "secret123".to_string(), + amount: 100u128.into(), + }), + swap_operations: vec![ + SwapOperation { + pool: "pool_1".to_string(), + denom_in: "secret123".to_string(), + denom_out: "ua".to_string(), + interface: None, + } + ], + expected_messages: vec![ + + SubMsg { + id: 0, + gas_limit: None, + reply_on: Never, + msg: WasmMsg::Execute { + contract_addr: "secret123".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&snip20::ExecuteMsg::Send { + amount: Uint128::from(100u128), + recipient: "shade_router".to_string(), + recipient_code_hash: Some("code_hash".to_string()), + memo: None, + padding: None, + msg: Some(to_binary(&shade_router::InvokeMsg::SwapTokensForExact { + path: vec![shade_router::Hop { + addr: "pool_1".to_string(), + code_hash: "code_hash".to_string(), + }], + expected_return: None, + recipient: None, + })?), + })?, + funds: vec![], + }.into(), + }, + SubMsg { + id: 0, + gas_limit: None, + reply_on: Never, + msg: WasmMsg::Execute { + contract_addr: "swap_contract_address".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&ExecuteMsg::TransferFundsBack { + swapper: Addr::unchecked("entry_point"), + return_denom: "ua".to_string(), + })?, + funds: vec![], + }.into(), + }, + ], + expected_error: None, + }; + "One Swap Operation - Snip20 In & Out")] +#[test_case( + Params { + caller: "entry_point".to_string(), + info_funds: vec![ + Coin::new(100, "un"), + ], + sent_asset: Asset::Cw20(Cw20Coin { + address: "secret123".to_string(), + amount: 100u128.into() + }), + swap_operations: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::SwapOperationsEmpty), + }; + "Coin sent with cw20 - Expect Error")] +fn test_execute_swap(params: Params) -> ContractResult<()> { + // Create mock dependencies + let mut deps = mock_dependencies(); + + // Create mock wasm handler to handle the swap adapter contract query + let wasm_handler = |query: &WasmQuery| -> QuerierResult { + match query { + WasmQuery::Smart { contract_addr, .. } => { + if contract_addr == "secret123" { + SystemResult::Ok(SystemContractResult::Ok( + to_binary(&snip20::BalanceResponse { + amount: 100u128.into(), + }) + .unwrap(), + )) + } else { + panic!("Unsupported contract: {:?}", query); + } + } + _ => panic!("Unsupported query: {:?}", query), + } + }; + + // Update querier with mock wasm handler + deps.querier.update_wasm(wasm_handler); + + // Create mock env + let mut env = mock_env(); + env.contract.address = Addr::unchecked("swap_contract_address"); + env.contract.code_hash = "code_hash".to_string(); + + // Convert info funds vector into a slice of Coin objects + let info_funds: &[Coin] = ¶ms.info_funds; + + // Create mock info with entry point contract address + let info = mock_info(params.sent_asset.denom(), info_funds); + + // Store the entry point contract address + ENTRY_POINT_CONTRACT.save( + deps.as_mut().storage, + &ContractInfo { + address: Addr::unchecked("entry_point"), + code_hash: "code_hash".to_string(), + }, + )?; + SHADE_ROUTER_CONTRACT.save( + deps.as_mut().storage, + &ContractInfo { + address: Addr::unchecked("shade_router"), + code_hash: "code_hash".to_string(), + }, + )?; + SHADE_POOL_CODE_HASH.save(deps.as_mut().storage, &"code_hash".to_string())?; + REGISTERED_TOKENS.save( + deps.as_mut().storage, + Addr::unchecked("secret123"), + &ContractInfo { + address: Addr::unchecked("secret123"), + code_hash: "code_hash".to_string(), + }, + )?; + + // Call execute_swap with the given test parameters + let res = skip_go_swap_adapter_shade_protocol::contract::execute( + deps.as_mut(), + env, + info, + ExecuteMsg::Receive(snip20::Snip20ReceiveMsg { + sender: Addr::unchecked(params.caller), + amount: params.sent_asset.amount(), + from: Addr::unchecked("entry_point".to_string()), + memo: None, + msg: Some(to_binary(&Snip20HookMsg::Swap { + operations: params.swap_operations, + })?), + }), + ); + + // Assert the behavior is correct + match res { + Ok(res) => { + // Assert the test did not expect an error + assert!( + params.expected_error.is_none(), + "expected test to error with {:?}, but it succeeded", + params.expected_error + ); + + // Assert the messages are correct + assert_eq!(res.messages, params.expected_messages); + } + Err(err) => { + // Assert the test expected an error + assert!( + params.expected_error.is_some(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err, params.expected_error.unwrap()); + } + } + + Ok(()) +} diff --git a/contracts/adapters/swap/shade-protocol/tests/test_execute_swap.rs b/contracts/adapters/swap/shade-protocol/tests/test_execute_swap.rs new file mode 100644 index 00000000..83e0772c --- /dev/null +++ b/contracts/adapters/swap/shade-protocol/tests/test_execute_swap.rs @@ -0,0 +1,325 @@ +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env, mock_info}, + to_binary, Addr, Coin, ContractInfo, + ReplyOn::Never, + SubMsg, WasmMsg, +}; +use secret_skip::{ + asset::Asset, + cw20::Cw20Coin, + snip20::{self, Snip20ReceiveMsg}, + swap::SwapOperation, +}; +use skip_go_swap_adapter_shade_protocol::{ + error::{ContractError, ContractResult}, + msg::{ExecuteMsg, Snip20HookMsg}, + shade_swap_router_msg as shade_router, + state::{ENTRY_POINT_CONTRACT, REGISTERED_TOKENS, SHADE_POOL_CODE_HASH, SHADE_ROUTER_CONTRACT}, +}; +use test_case::test_case; + +/* +Test Cases: + +Expect Success + - One Swap Operation + - Multiple Swap Operations + - No Swap Operations (This is prevented in the entry point contract; and will not add any swap messages to the response) + +Expect Error + - Unauthorized Caller (Only the stored entry point contract can call this function) + - No Coin Sent + - More Than One Coin Sent + + */ + +// Define test parameters +struct Params { + caller: String, + sent_asset: Asset, + swap_operations: Vec, + expected_messages: Vec, + expected_error: Option, +} + +// Test execute_swap +#[test_case( + Params { + caller: "entry_point".to_string(), + sent_asset: Asset::Cw20(Cw20Coin { + address: "secret123".to_string(), + amount: 100u128.into(), + }), + swap_operations: vec![ + SwapOperation { + pool: "pool_1".to_string(), + denom_in: "secret123".to_string(), + denom_out: "secret456".to_string(), + interface: None, + } + ], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "secret123".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&snip20::ExecuteMsg::Send { + recipient: "shade_router".to_string(), + recipient_code_hash: Some("code_hash".to_string()), + amount: 100u128.into(), + msg: Some(to_binary(&shade_router::InvokeMsg::SwapTokensForExact { + path: vec![shade_router::Hop { + addr: "pool_1".to_string(), + code_hash: "code_hash".to_string(), + }], + expected_return: None, + recipient: None, + }).unwrap()), + memo: None, + padding: None, + })?, + funds: vec![], + }.into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_contract_address".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&ExecuteMsg::TransferFundsBack { + return_denom: "secret456".to_string(), + swapper: Addr::unchecked("entry_point"), + })?, + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "One Swap Operation")] +/* +#[test_case( + Params { + caller: "entry_point".to_string(), + sent_asset: Asset::Cw20(Cw20Coin { + address: "secret123".to_string(), + amount: 100u128.into(), + }), + swap_operations: vec![ + SwapOperation { + pool: "pool_1".to_string(), + denom_in: "secret123".to_string(), + denom_out: "secret456".to_string(), + interface: None, + }, + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "secret456".to_string(), + denom_out: "secret789".to_string(), + interface: None, + } + ], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_contract_address".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&ExecuteMsg::AstroportPoolSwap { + operation: SwapOperation { + pool: "pool_1".to_string(), + denom_in: "secret123".to_string(), + denom_out: "secret456".to_string(), + interface: None, + } + })?, + funds: vec![], + }.into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_contract_address".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&ExecuteMsg::AstroportPoolSwap { + operation: SwapOperation { + pool: "pool_2".to_string(), + denom_in: "secret456".to_string(), + denom_out: "secret789".to_string(), + interface: None, + } + })?, + funds: vec![], + }.into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_contract_address".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&ExecuteMsg::TransferFundsBack { + return_denom: "secret789".to_string(), + swapper: Addr::unchecked("entry_point"), + })?, + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Multiple Swap Operations")] +#[test_case( + Params { + caller: "entry_point".to_string(), + sent_asset: Asset::Cw20(Cw20Coin { + address: "secret123".to_string(), + amount: 100u128.into(), + }), + swap_operations: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::SwapOperationsEmpty), + }; + "No Swap Operations")] +#[test_case( + Params { + caller: "entry_point".to_string(), + sent_asset: Asset::Cw20(Cw20Coin { + address: "secret123".to_string(), + amount: 100u128.into(), + }), + swap_operations: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Payment(cw_utils::PaymentError::NoFunds{})), + }; + "No Coin Sent - Expect Error")] +#[test_case( + Params { + caller: "random".to_string(), + sent_asset: Asset::Cw20(Cw20Coin { + address: "secret123".to_string(), + amount: 100u128.into(), + }), + swap_operations: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Unauthorized), + }; + "Unauthorized Caller - Expect Error")] +*/ +fn test_execute_swap(params: Params) -> ContractResult<()> { + // Create mock dependencies + let mut deps = mock_dependencies(); + + // Create mock env + let mut env = mock_env(); + env.contract.address = Addr::unchecked("swap_contract_address"); + env.contract.code_hash = "code_hash".to_string(); + + // Store the entry point contract address + ENTRY_POINT_CONTRACT.save( + deps.as_mut().storage, + &ContractInfo { + address: Addr::unchecked("entry_point"), + code_hash: "code_hash".to_string(), + }, + )?; + SHADE_ROUTER_CONTRACT.save( + deps.as_mut().storage, + &ContractInfo { + address: Addr::unchecked("shade_router".to_string()), + code_hash: "code_hash".to_string(), + }, + )?; + SHADE_POOL_CODE_HASH + .save(deps.as_mut().storage, &"code_hash".to_string()) + .unwrap(); + + REGISTERED_TOKENS + .save( + deps.as_mut().storage, + Addr::unchecked("secret123".to_string()), + &ContractInfo { + address: Addr::unchecked("secret123".to_string()), + code_hash: "code_hash".to_string(), + }, + ) + .unwrap(); + + REGISTERED_TOKENS + .save( + deps.as_mut().storage, + Addr::unchecked("secret456".to_string()), + &ContractInfo { + address: Addr::unchecked("secret456".to_string()), + code_hash: "code_hash".to_string(), + }, + ) + .unwrap(); + + REGISTERED_TOKENS + .save( + deps.as_mut().storage, + Addr::unchecked("secret789".to_string()), + &ContractInfo { + address: Addr::unchecked("secret789".to_string()), + code_hash: "code_hash".to_string(), + }, + ) + .unwrap(); + + // Call execute_swap with the given test parameters + let res = skip_go_swap_adapter_shade_protocol::contract::execute( + deps.as_mut(), + env, + mock_info(¶ms.sent_asset.denom(), &vec![]), + ExecuteMsg::Receive(Snip20ReceiveMsg { + sender: Addr::unchecked(params.caller.clone()), + from: Addr::unchecked(params.caller), + amount: params.sent_asset.amount(), + memo: None, + msg: Some(to_binary(&Snip20HookMsg::Swap { + operations: params.swap_operations.clone(), + })?), + }), + ); + + // Assert the behavior is correct + match res { + Ok(res) => { + // Assert the test did not expect an error + assert!( + params.expected_error.is_none(), + "expected test to error with {:?}, but it succeeded", + params.expected_error + ); + + // Assert the messages are correct + assert_eq!(res.messages, params.expected_messages); + } + Err(err) => { + // Assert the test expected an error + assert!( + params.expected_error.is_some(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err, params.expected_error.unwrap()); + } + } + + Ok(()) +} diff --git a/contracts/adapters/swap/shade-protocol/tests/test_execute_transfer_funds_back.rs b/contracts/adapters/swap/shade-protocol/tests/test_execute_transfer_funds_back.rs new file mode 100644 index 00000000..1097c833 --- /dev/null +++ b/contracts/adapters/swap/shade-protocol/tests/test_execute_transfer_funds_back.rs @@ -0,0 +1,226 @@ +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env, mock_info}, + to_binary, Addr, BankMsg, Coin, ContractInfo, ContractResult as SystemContractResult, + QuerierResult, + ReplyOn::Never, + SubMsg, SystemResult, Uint128, WasmMsg, WasmQuery, +}; +use secret_skip::snip20; +use skip_go_swap_adapter_shade_protocol::{ + error::{ContractError, ContractResult}, + msg::ExecuteMsg, + state::{ENTRY_POINT_CONTRACT, REGISTERED_TOKENS, VIEWING_KEY}, +}; +use test_case::test_case; + +/* +Test Cases: + +Expect Success + - One Coin Balance + - Multiple Coin Balance + - No Coin Balance (This will fail at the bank module if attempted) + +Expect Error + - Unauthorized Caller (Only contract itself can call this function) + */ + +// Define test parameters +struct Params { + caller: String, + contract_balance: Vec, + return_denom: String, + expected_messages: Vec, + expected_error: Option, +} + +// Test execute_transfer_funds_back +#[test_case( + Params { + caller: "swap_contract_address".to_string(), + contract_balance: vec![Coin::new(100, "secret123")], + return_denom: "secret123".to_string(), + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "secret123".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&snip20::ExecuteMsg::Send { + recipient: "swapper".to_string(), + recipient_code_hash: None, + amount: 100u128.into(), + msg: None, + memo: None, + padding: None, + })?, + funds: vec![], + }.into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Transfers One Coin Balance")] +/* +#[test_case( + Params { + caller: "swap_contract_address".to_string(), + contract_balance: vec![ + Coin::new(100, "secret123"), + Coin::new(100, "secret456"), + ], + return_denom: "secret123".to_string(), + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "secret123".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&snip20::ExecuteMsg::Send { + recipient: "swapper".to_string(), + recipient_code_hash: None, + amount: 100u128.into(), + msg: None, + memo: None, + padding: None, + })?, + funds: vec![], + }.into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Transfers Multiple Coin Balance")] +*/ +#[test_case( + Params { + caller: "random".to_string(), + contract_balance: vec![], + return_denom: "secret123".to_string(), + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "swapper".to_string(), + amount: vec![], + }.into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: Some(ContractError::Unauthorized), + }; + "Unauthorized Caller")] +fn test_execute_transfer_funds_back(params: Params) -> ContractResult<()> { + // Create mock dependencies + let mut deps = mock_dependencies(); + + let contract_balance = params.contract_balance.clone(); + + // Mock contract balance querys + let wasm_handler = move |query: &WasmQuery| -> QuerierResult { + match query { + WasmQuery::Smart { contract_addr, .. } => { + match contract_balance + .iter() + .find(|coin| coin.denom == *contract_addr) + { + Some(coin) => SystemResult::Ok(SystemContractResult::Ok( + to_binary(&snip20::QueryResponse::Balance { + amount: coin.amount.u128().into(), + }) + .unwrap(), + )), + None => SystemResult::Ok(SystemContractResult::Ok( + to_binary(&snip20::QueryResponse::Balance { + amount: 0u128.into(), + }) + .unwrap(), + )), + } + } + _ => panic!("Unsupported query: {:?}", query), + } + }; + + // Update querier with mock wasm handler + deps.querier.update_wasm(wasm_handler); + + // Create mock env + let mut env = mock_env(); + env.contract.address = Addr::unchecked("swap_contract_address"); + env.contract.code_hash = "code_hash".to_string(); + + // Create mock info + let info = mock_info(¶ms.caller.clone(), &[]); + + VIEWING_KEY.save(&mut deps.storage, &"viewing_key".to_string())?; + ENTRY_POINT_CONTRACT.save( + &mut deps.storage, + &ContractInfo { + address: Addr::unchecked("entry_point_contract"), + code_hash: "code_hash".to_string(), + }, + )?; + REGISTERED_TOKENS.save( + &mut deps.storage, + Addr::unchecked(params.return_denom.clone()), + &ContractInfo { + address: Addr::unchecked(params.return_denom.clone()), + code_hash: "code_hash".to_string(), + }, + )?; + for coin in params.contract_balance.iter() { + REGISTERED_TOKENS.save( + &mut deps.storage, + Addr::unchecked(coin.denom.clone()), + &ContractInfo { + address: Addr::unchecked(coin.denom.clone()), + code_hash: "code_hash".to_string(), + }, + )?; + } + + // Call execute_swap with the given test parameters + let res = skip_go_swap_adapter_shade_protocol::contract::execute( + deps.as_mut(), + env, + info, + ExecuteMsg::TransferFundsBack { + return_denom: params.return_denom, + swapper: Addr::unchecked("swapper"), + }, + ); + + // Assert the behavior is correct + match res { + Ok(res) => { + // Assert the test did not expect an error + assert!( + params.expected_error.is_none(), + "expected test to error with {:?}, but it succeeded", + params.expected_error + ); + + // Assert the messages are correct + assert_eq!(res.messages, params.expected_messages); + } + Err(err) => { + // Assert the test expected an error + assert!( + params.expected_error.is_some(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err, params.expected_error.unwrap()); + } + } + + Ok(()) +} diff --git a/contracts/secret-entry-point/Cargo.toml b/contracts/secret-entry-point/Cargo.toml new file mode 100644 index 00000000..5a6b2b87 --- /dev/null +++ b/contracts/secret-entry-point/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "skip-go-secret-entry-point" +version = { workspace = true } +rust-version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +documentation = { workspace = true } +keywords = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-schema = { workspace = true } +skip-go-swap-adapter-shade-protocol = { path = "../adapters/swap/shade-protocol" } +secret-skip = { workspace = true } +#cosmwasm-std = { workspace = true } +#cw2 = { workspace = true } +#cw20 = { workspace = true } +#cw-storage-plus = { workspace = true } +#cw-utils = { workspace = true } +#skip = { workspace = true } +thiserror = { workspace = true } +serde = "1.0.114" +schemars = "0.8.1" +secret-toolkit = { git = "https://github.com/scrtlabs/secret-toolkit", tag = "v0.10.0" } +secret-storage-plus = { git = "https://github.com/securesecrets/secret-plus-utils", tag = "v0.1.1", features = [] } +cosmwasm-std = { package = "secret-cosmwasm-std", version = "1.1.11"} + +[dev-dependencies] +test-case = { workspace = true } +serde_json = "1.0.64" diff --git a/contracts/secret-entry-point/README.md b/contracts/secret-entry-point/README.md new file mode 100644 index 00000000..15676583 --- /dev/null +++ b/contracts/secret-entry-point/README.md @@ -0,0 +1,268 @@ +# Entry Point Contract + +The entry point contract is responsible for providing a standardized interface (w/ safety checks) to interact with Skip Swap across all CosmWasm-enabled chains. The contract: +1. Performs basic validation on the call data +2. If a fee swap is provided, queries the swap adapter contract to determine how much of the coin sent with the contract call is needed to receive the required fee coin(s), and dispatches the swap. +3. Dispatches the user swap provided in the call data to the relevant swap adapter contract. +4. Handles affiliate fee payments if provided. +5. Verifies the amount out received from the swap(s) is greater than the minimum amount required by the caller after all fees have been subtracted (swap, ibc, affiliate) +6. Dispatches one of the following post-swap actions with the received funds from the swap: + - Transfer to an address on the same chain + - IBC transfer to an address on a different chain (which allows for multi-hop IBC transfers or contract calls if the destination chains support it) + - Call a contract on the same chain + +WARNING: Do not send funds directly to the entry point contract without calling one of its functions. Funds sent directly to the contract do not trigger any contract logic that performs validation / safety checks (as the Cosmos SDK handles direct fund transfers in the `Bank` module and not the `Wasm` module). There are no explicit recovery mechanisms for accidentally sent funds. + +## InstantiateMsg + +Instantiates a new entry point contract using the adapter contracts provided in the instantiation message. + +``` json +{ + "swap_venues": [ + { + "name": "neutron-astroport", + "adapter_contract_address": "neutron..." + } + ], + "ibc_transfer_contract_address": "neutron..." +} +``` + +## ExecuteMsg + +### `swap_and_action` + +Swaps the coin sent and performs a post-swap action. + +Optional fields: +- `fee_swap` is used if a fee is required by the IBC transfer. + +Notes: +- Only one coin can be sent to the contract when calling `swap_and_action` otherwise the transaction will fail. +- `timeout_timestamp` is Unix epoch time in nanoseconds. The transaction will fail if the `timeout_timestamp` has passed when the contract is called. +- `post_swap_action` can be one of three actions: `bank_send`, `ibc_transfer`, or `contract_call`. + - `bank_send`: Sends the assets received from the `user_swap` to an address on the same chain the swap occured on. + - `ibc_transfer`: ICS-20 transfers the assets received from the swap(s) to an address on a different chain than the swap occured on. The ICS-20 transfer supports including a memo in the outgoing transfer, allowing for multi-hop transfers via Packet Forward Middleware and/or contract calls via IBC-hooks. + - `contract_call`: Calls a contract on the same chain the swap occured, using the assets received from the swap as the contract call's funds. +- `affiliates` is a list of affiliates that will take a fee (in basis points) from the `min_coin` provided. If no affiliates are associated with a call then an empty list is to be provided. +- The vector of coins provided in `ibc_info.fee` must all be the same denom. +- A `fee_swap` is only valid if the `post_swap_action` is an `ibc_transfer` with a provided `ibc_info.fee`. The `coin_out` used for the fee swap is dervied from the provided `ibc_info.fee`. +- The `coin_in` used in the `user_swap` is derived based on the coin sent to the contract from the user's contract call, after accounting for the fee swap and if the `user_swap` is a `SwapExactCoinIn` or `SwapExactCoinOut` + +#### Examples + +SwapExactCoinIn: + +``` json +{ + "swap_and_action": { + "user_swap": { + "swap_exact_coin_in": { + "swap_venue_name": "neutron-astroport", + "operations": [ + { + "pool": "neutron...", + "denom_in": "uatom", + "denom_out": "untrn" + }, + { + "pool": "neutron...", + "denom_in": "untrn", + "denom_out": "uosmo" + } + ] + }, + }, + "min_coin": { + "denom": "uosmo", + "amount": "1000000" + }, + "timeout_timestamp": 1000000000000, + "post_swap_action": { + "ibc_transfer": { + "ibc_info": { + "source_channel": "channel-1", + "receiver": "cosmos...", + "fee": { + "recv_fee": [], + "ack_fee": [ + { + "denom": "untrn", + "amount": "100" + } + ], + "timeout_fee": [ + { + "denom": "untrn", + "amount": "100" + } + ] + }, + "memo": "", + "recover_address": "neutron..." + } + "fee_swap": { + "swap_venue_name": "neutron-astroport", + "operations": [ + { + "pool": "neutron...", + "denom_in": "uatom", + "denom_out": "untrn" + } + ] + }, + } + }, + "affiliates": [ + { + "basis_points_fee": 10, + "address": "neutron..." + } + ] + } +} +``` + +SwapExactCoinOut: + +``` json +{ + "swap_and_action": { + "user_swap": { + "swap_exact_coin_out": { + "swap_venue_name": "neutron-astroport", + "operations": [ + { + "pool": "neutron...", + "denom_in": "uatom", + "denom_out": "untrn" + }, + { + "pool": "neutron...", + "denom_in": "untrn", + "denom_out": "uosmo" + } + ], + "refund_address": "neutron..." + }, + }, + "min_coin": { + "denom": "uosmo", + "amount": "1000000" + }, + "timeout_timestamp": 1000000000000, + "post_swap_action": { + "bank_send": { + "to_address": "neutron..." + } + }, + "affiliates": [ + { + "basis_points_fee": 10, + "address": "neutron..." + } + ] + } +} +``` + +### `user_swap` + +Dispatches the user swap to the relevant swap adapter contract and affiliate fee bank send messages. If the user swap is a `SwapExactCoinOut` it also dispatches the refund bank send message to the provided `refund_address` + +Note: Can only be called by the entry point contract itself, any external calls to this function will fail. + +``` json +{ + "user_swap": { + "swap": { + "swap_exact_coin_out": { + "swap_venue_name": "neutron-astroport", + "operations": [ + { + "pool": "neutron...", + "denom_in": "uatom", + "denom_out": "untrn" + }, + { + "pool": "neutron...", + "denom_in": "untrn", + "denom_out": "uosmo" + } + ], + "refund_address": "neutron..." + }, + }, + "min_coin": { + "denom": "uosmo", + "amount": "1000000" + }, + "remaining_coin": { + "denom": "uatom", + "amount": "100000" + }, + "affiliates": [] + } +} +``` + +### `post_swap_action` + +Performs a post swap action. + +Note: Can only be called by the entry point contract itself, any external calls to this function will fail. + +``` json +{ + "post_swap_action": { + "min_coin": { + "denom": "uosmo", + "amount": "1000000" + }, + "timeout_timestamp": 1000000000000, + "post_swap_action": { + "bank_send": { + "to_address": "neutron..." + } + }, + "exact_out": false, + } +} +``` + +## QueryMsg + +### `swap_venue_adapter_contract` + +Returns the swap adapter contract set at instantiation for the given swap venue name provided as an argument. + +Query: +``` json +{ + "swap_venue_adapter_contract": { + "name": "neutron-astroport" + } +} +``` + +Response: +``` json +"neutron..." +``` + +### `ibc_transfer_adapter_contract` + +Returns the IBC transfer adapter contract set at instantiation, requires no arguments. + +Query: +``` json +{ + "ibc_transfer_adapter_contract": {} +} +``` + +Response: +``` json +"neutron..." +``` \ No newline at end of file diff --git a/contracts/secret-entry-point/src/bin/schema.rs b/contracts/secret-entry-point/src/bin/schema.rs new file mode 100644 index 00000000..8599f00e --- /dev/null +++ b/contracts/secret-entry-point/src/bin/schema.rs @@ -0,0 +1,10 @@ +use cosmwasm_schema::write_api; +use skip_go_secret_entry_point::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg + } +} diff --git a/contracts/secret-entry-point/src/contract.rs b/contracts/secret-entry-point/src/contract.rs new file mode 100644 index 00000000..85148c7c --- /dev/null +++ b/contracts/secret-entry-point/src/contract.rs @@ -0,0 +1,315 @@ +use crate::{ + error::{ContractError, ContractResult}, + execute::{ + execute_action, execute_action_with_recover, execute_post_swap_action, + execute_swap_and_action, execute_swap_and_action_with_recover, execute_user_swap, + receive_snip20, + }, + msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, + query::{query_ibc_transfer_adapter_contract, query_swap_venue_adapter_contract}, + reply::{reply_swap_and_action_with_recover, RECOVER_REPLY_ID}, + state::{ + BLOCKED_CONTRACT_ADDRESSES, IBC_TRANSFER_CONTRACT, REGISTERED_TOKENS, SWAP_VENUE_MAP, + VIEWING_KEY, + }, +}; +use cosmwasm_std::{ + entry_point, to_binary, Binary, ContractInfo, Deps, DepsMut, Env, MessageInfo, Reply, Response, + StdResult, +}; +use secret_toolkit::snip20; + +/////////////// +/// MIGRATE /// +/////////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> ContractResult { + unimplemented!() +} + +/////////////////// +/// INSTANTIATE /// +/////////////////// + +// Contract name and version used for migration. +/* +const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +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, +) -> ContractResult { + // Set contract version + // set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + // Create response object to return + let mut response: Response = Response::new().add_attribute("action", "instantiate"); + + // Insert the entry point contract address into the blocked contract addresses map + BLOCKED_CONTRACT_ADDRESSES.save(deps.storage, &env.contract.address, &())?; + VIEWING_KEY.save(deps.storage, &msg.viewing_key)?; + + // Iterate through the swap venues provided and create a map of venue names to swap adapter contract addresses + for swap_venue in msg.swap_venues.iter() { + // Validate the swap contract address + let checked_swap_contract = ContractInfo { + address: deps + .api + .addr_validate(&swap_venue.adapter_contract.address.to_string())?, + code_hash: swap_venue.adapter_contract.code_hash.clone(), + }; + + // Prevent duplicate swap venues by erroring if the venue name is already stored + if SWAP_VENUE_MAP.has(deps.storage, &swap_venue.name) { + return Err(ContractError::DuplicateSwapVenueName); + } + + // Store the swap venue name and contract address inside the swap venue map + SWAP_VENUE_MAP.save(deps.storage, &swap_venue.name, &checked_swap_contract)?; + + // Insert the swap contract address into the blocked contract addresses map + BLOCKED_CONTRACT_ADDRESSES.save(deps.storage, &checked_swap_contract.address, &())?; + + // Add the swap venue and contract address to the response + response = response + .add_attribute("action", "add_swap_venue") + .add_attribute("name", &swap_venue.name) + .add_attribute("contract_address", &checked_swap_contract.address); + } + + // Validate ibc transfer adapter contract addresses + let checked_ibc_transfer_contract = ContractInfo { + address: deps + .api + .addr_validate(&msg.ibc_transfer_contract.address.to_string())?, + code_hash: msg.ibc_transfer_contract.code_hash.clone(), + }; + + // Store the ibc transfer adapter contract address + IBC_TRANSFER_CONTRACT.save(deps.storage, &checked_ibc_transfer_contract)?; + + // Insert the ibc transfer adapter contract address into the blocked contract addresses map + BLOCKED_CONTRACT_ADDRESSES.save(deps.storage, &checked_ibc_transfer_contract.address, &())?; + + // Add the ibc transfer adapter contract address to the response + response = response + .add_attribute("action", "add_ibc_transfer_adapter") + .add_attribute("contract_address", &checked_ibc_transfer_contract.address); + + // If the hyperlane transfer contract address is provided, validate and store it + /* + if let Some(hyperlane_transfer_contract) = msg.hyperlane_transfer_contract { + // Validate hyperlane transfer adapter contract address + let checked_hyperlane_transfer_contract = ContractInfo { + address: deps + .api + .addr_validate(&hyperlane_transfer_contract.address.to_string())?, + code_hash: hyperlane_transfer_contract.code_hash.clone(), + }; + + // Store the hyperlane transfer adapter contract address + HYPERLANE_TRANSFER_CONTRACT_ADDRESS + .save(deps.storage, &checked_hyperlane_transfer_contract)?; + + // Insert the hyperlane transfer adapter contract address into the blocked contract addresses map + BLOCKED_CONTRACT_ADDRESSES.save( + deps.storage, + &checked_hyperlane_transfer_contract.address, + &(), + )?; + + // Add the hyperlane transfer adapter contract address to the response + response = response + .add_attribute("action", "add_hyperlane_transfer_adapter") + .add_attribute( + "contract_address", + &checked_hyperlane_transfer_contract.address, + ); + } + */ + + Ok(response) +} + +/////////////// +/// EXECUTE /// +/////////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> ContractResult { + match msg { + ExecuteMsg::RegisterTokens { contracts } => register_tokens(deps, env, contracts), + ExecuteMsg::Receive(msg) => receive_snip20(deps, env, info, msg), + ExecuteMsg::SwapAndActionWithRecover { + sent_asset, + user_swap, + min_asset, + timeout_timestamp, + post_swap_action, + affiliates, + recovery_addr, + } => execute_swap_and_action_with_recover( + deps, + env, + info, + sent_asset, + user_swap, + min_asset, + timeout_timestamp, + post_swap_action, + affiliates, + recovery_addr, + ), + ExecuteMsg::SwapAndAction { + sent_asset, + user_swap, + min_asset, + timeout_timestamp, + post_swap_action, + affiliates, + } => execute_swap_and_action( + deps, + env, + info, + sent_asset, + user_swap, + min_asset, + timeout_timestamp, + post_swap_action, + affiliates, + ), + ExecuteMsg::UserSwap { + swap, + min_asset, + remaining_asset, + affiliates, + } => execute_user_swap( + deps, + env, + info, + swap, + min_asset, + remaining_asset, + affiliates, + ), + ExecuteMsg::PostSwapAction { + min_asset, + timeout_timestamp, + post_swap_action, + exact_out, + } => execute_post_swap_action( + deps, + env, + info, + min_asset, + timeout_timestamp, + post_swap_action, + exact_out, + ), + ExecuteMsg::Action { + sent_asset, + timeout_timestamp, + action, + exact_out, + min_asset, + } => execute_action( + deps, + env, + info, + sent_asset, + timeout_timestamp, + action, + exact_out, + min_asset, + ), + ExecuteMsg::ActionWithRecover { + sent_asset, + timeout_timestamp, + action, + exact_out, + min_asset, + recovery_addr, + } => execute_action_with_recover( + deps, + env, + info, + sent_asset, + timeout_timestamp, + action, + exact_out, + min_asset, + recovery_addr, + ), + } +} + +fn register_tokens( + deps: DepsMut, + env: Env, + contracts: Vec, +) -> ContractResult { + let mut response = Response::new(); + + let viewing_key = VIEWING_KEY.load(deps.storage)?; + + for contract in contracts.iter() { + // Add to storage for later use of code hash + REGISTERED_TOKENS.save(deps.storage, contract.address.clone(), contract)?; + // register receive, set viewing key, & add attribute + response = response + .add_attribute("register_token", contract.address.clone()) + .add_messages(vec![ + snip20::set_viewing_key_msg( + viewing_key.clone(), + None, + 255, + contract.code_hash.clone(), + contract.address.to_string(), + )?, + snip20::register_receive_msg( + env.contract.code_hash.clone(), + None, + 255, + contract.code_hash.clone(), + contract.address.to_string(), + )?, + ]); + } + + Ok(response) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { + match msg.id { + RECOVER_REPLY_ID => reply_swap_and_action_with_recover(deps, msg), + _ => Err(ContractError::ReplyIdError(msg.id)), + } +} + +///////////// +/// QUERY /// +///////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::SwapVenueAdapterContract { name } => { + to_binary(&query_swap_venue_adapter_contract(deps, name)?) + } + QueryMsg::IbcTransferAdapterContract {} => { + to_binary(&query_ibc_transfer_adapter_contract(deps)?) + } + } +} diff --git a/contracts/secret-entry-point/src/error.rs b/contracts/secret-entry-point/src/error.rs new file mode 100644 index 00000000..2a62f35e --- /dev/null +++ b/contracts/secret-entry-point/src/error.rs @@ -0,0 +1,106 @@ +use cosmwasm_std::{Addr, OverflowError, StdError}; +use secret_skip::error::SkipError; +use thiserror::Error; + +pub type ContractResult = core::result::Result; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + /////////////// + /// GENERAL /// + /////////////// + + #[error(transparent)] + Std(#[from] StdError), + + #[error(transparent)] + Skip(#[from] SkipError), + + #[error(transparent)] + Overflow(#[from] OverflowError), + + /* + #[error(transparent)] + Payment(#[from] cw_utils::PaymentError), + */ + #[error("Unauthorized")] + Unauthorized, + + #[error("Timeout Timestamp Less Than Current Timestamp")] + Timeout, + + #[error("Duplicate Swap Venue Name Provided")] + DuplicateSwapVenueName, + + #[error("IBC fee denom differs from asset received without a fee swap to convert")] + IBCFeeDenomDiffersFromAssetReceived, + + //////////////// + /// FEE SWAP /// + //////////////// + + #[error("Fee Swap Not Allowed: No IBC Fees Provided")] + FeeSwapWithoutIbcFees, + + #[error("Fee Swap Asset In Denom Differs From Asset Sent To Contract")] + FeeSwapAssetInDenomMismatch, + + ///////////////// + /// USER SWAP /// + ///////////////// + + #[error("User Swap Asset In Denom Differs From Asset Sent To Contract")] + UserSwapAssetInDenomMismatch, + + #[error("No Refund Address Provided For Swap Exact Asset Out User Swap")] + NoRefundAddress, + + //////////////////////// + /// POST SWAP ACTION /// + //////////////////////// + + #[error("Received Less Asset From Swaps Than Minimum Asset Required")] + ReceivedLessAssetFromSwapsThanMinAsset, + + #[error("Contract Call Address Cannot Be The Entry Point Or Adapter Contracts")] + ContractCallAddressBlocked, + + #[error( + "IBC Transfer Adapter Only Supports Native Coins, Cw20 IBC Transfers Are Contract Calls" + )] + NonNativeIbcTransfer, + + #[error("Hyperlane Transfer Adapter Only Supports Native Coins")] + NonNativeHplTransfer, + + #[error("Reply id: {0} not valid")] + ReplyIdError(u64), + + ////////////////// + /// ACTION /// + ////////////////// + + #[error("No Minimum Asset Provided with Exact Out Action")] + NoMinAssetProvided, + + #[error("Sent Asset and Min Asset Denoms Do Not Match with Exact Out Action")] + ActionDenomMismatch, + + #[error("Remaining Asset Less Than Min Asset with Exact Out Action")] + RemainingAssetLessThanMinAsset, + + #[error("No Snip20 Receive Msg Provided")] + NoSnip20ReceiveMsg, + + #[error("Native Coin Not Supported")] + NativeCoinNotSupported, + + #[error("Invalid Snip20 Sender")] + InvalidSnip20Sender, + + #[error("Snip20 Token Not Registered {0}")] + TokenNotRegistered(Addr), + + #[error("Unsupported Action")] + UnsupportedAction, +} diff --git a/contracts/secret-entry-point/src/execute.rs b/contracts/secret-entry-point/src/execute.rs new file mode 100644 index 00000000..319a14c3 --- /dev/null +++ b/contracts/secret-entry-point/src/execute.rs @@ -0,0 +1,1130 @@ +use std::vec; + +use crate::{ + error::{ContractError, ContractResult}, + //hyperlane::{ExecuteMsg as HplExecuteMsg, ExecuteMsg::HplTransfer}, + msg::{Action, Affiliate, ExecuteMsg, Snip20HookMsg}, + reply::{RecoverTempStorage, RECOVER_REPLY_ID}, + state::{ + BLOCKED_CONTRACT_ADDRESSES, IBC_TRANSFER_CONTRACT, PRE_SWAP_OUT_ASSET_AMOUNT, + RECOVER_TEMP_STORAGE, REGISTERED_TOKENS, SWAP_VENUE_MAP, VIEWING_KEY, + }, +}; + +use secret_toolkit::snip20; + +use cosmwasm_std::{ + from_binary, to_binary, Addr, BankMsg, Coin, ContractInfo, CosmosMsg, DepsMut, Env, + MessageInfo, Response, StdError, SubMsg, Uint128, WasmMsg, +}; +use secret_skip::{ + asset::Asset, + cw20::Cw20Coin, + error::SkipError, + ibc::{self, IbcInfo}, + snip20::Snip20ReceiveMsg, + swap::{validate_swap_operations, Swap, SwapExactAssetOut}, +}; +use skip_go_swap_adapter_shade_protocol::msg::{ + QueryMsg as SwapQueryMsg, Snip20HookMsg as SwapHookMsg, +}; + +////////////////////////// +/// RECEIVE ENTRYPOINT /// +////////////////////////// + +// Receive is the main entry point for the contract to +// receive snip20 tokens and execute the swap and action message +pub fn receive_snip20( + deps: DepsMut, + env: Env, + info: MessageInfo, + snip20_msg: Snip20ReceiveMsg, +) -> ContractResult { + let sent_asset = Asset::Cw20(Cw20Coin { + address: info.sender.to_string(), + amount: snip20_msg.amount.u128().into(), + }); + + let msg = match snip20_msg.msg { + Some(msg) => msg, + None => { + return Err(ContractError::NoSnip20ReceiveMsg); + } + }; + match from_binary(&msg)? { + Snip20HookMsg::SwapAndActionWithRecover { + user_swap, + min_asset, + timeout_timestamp, + post_swap_action, + affiliates, + recovery_addr, + } => execute_swap_and_action_with_recover( + deps, + env, + info, + Some(sent_asset), + user_swap, + min_asset, + timeout_timestamp, + post_swap_action, + affiliates, + recovery_addr, + ), + Snip20HookMsg::SwapAndAction { + user_swap, + min_asset, + timeout_timestamp, + post_swap_action, + affiliates, + } => execute_swap_and_action( + deps, + env, + info, + Some(sent_asset), + user_swap, + min_asset, + timeout_timestamp, + post_swap_action, + affiliates, + ), + Snip20HookMsg::Action { + timeout_timestamp, + action, + exact_out, + min_asset, + } => execute_action( + deps, + env, + info, + Some(sent_asset), + timeout_timestamp, + action, + exact_out, + min_asset, + ), + Snip20HookMsg::ActionWithRecover { + timeout_timestamp, + action, + exact_out, + min_asset, + recovery_addr, + } => execute_action_with_recover( + deps, + env, + info, + Some(sent_asset), + timeout_timestamp, + action, + exact_out, + min_asset, + recovery_addr, + ), + } +} + +/////////////////////////// +/// EXECUTE ENTRYPOINTS /// +/////////////////////////// + +// Main entry point for the contract +// Dispatches the swap and post swap action +#[allow(clippy::too_many_arguments)] +pub fn execute_swap_and_action( + deps: DepsMut, + env: Env, + info: MessageInfo, + sent_asset: Option, + mut user_swap: Swap, + min_asset: Asset, + timeout_timestamp: u64, + post_swap_action: Action, + affiliates: Vec, +) -> ContractResult { + // Create a response object to return + let mut response: Response = Response::new().add_attribute("action", "execute_swap_and_action"); + + // Validate and unwrap the sent asset + let sent_asset = match sent_asset { + Some(sent_asset) => { + match &sent_asset { + Asset::Cw20(cw20) => { + if cw20.address != info.sender.to_string() { + return Err(ContractError::InvalidSnip20Sender); + } + } + Asset::Native(_) => { + return Err(ContractError::NativeCoinNotSupported); + } + } + // sent_asset.validate(&deps, &env, &info)?; + sent_asset + } + None => { + return Err(ContractError::NativeCoinNotSupported); + } + }; + + // Error if the current block time is greater than the timeout timestamp + if env.block.time.nanos() > timeout_timestamp { + return Err(ContractError::Timeout); + } + + let viewing_key = VIEWING_KEY.load(deps.storage)?; + let min_asset_contract = + REGISTERED_TOKENS.load(deps.storage, deps.api.addr_validate(min_asset.denom())?)?; + + // Save the current out asset amount to storage as the pre swap out asset amount + let pre_swap_out_asset_amount = match snip20::balance_query( + deps.querier, + env.contract.address.to_string(), + viewing_key, + 0, + min_asset_contract.code_hash.clone(), + min_asset_contract.address.to_string(), + ) { + Ok(balance) => balance.amount, + Err(e) => return Err(ContractError::Std(e)), + }; + PRE_SWAP_OUT_ASSET_AMOUNT.save(deps.storage, &pre_swap_out_asset_amount)?; + + // Already validated at entrypoints (both direct and snip20_receive) + let mut remaining_asset = sent_asset; + + // If the post swap action is an IBC transfer, then handle the ibc fees + // by either creating a fee swap message or deducting the ibc fees from + // the remaining asset received amount. + if let Action::IbcTransfer { ibc_info, fee_swap } = &post_swap_action { + response = + handle_ibc_transfer_fees(&deps, ibc_info, fee_swap, &mut remaining_asset, response)?; + } + + // Set a boolean to determine if the user swap is exact out or not + let exact_out = match &user_swap { + Swap::SwapExactAssetIn(_) => false, + Swap::SwapExactAssetOut(_) => true, + Swap::SmartSwapExactAssetIn(_) => false, + }; + + if let Swap::SmartSwapExactAssetIn(smart_swap) = &mut user_swap { + if smart_swap.routes.is_empty() { + return Err(ContractError::Skip(SkipError::RoutesEmpty)); + } + + match smart_swap + .amount() + .cmp(&remaining_asset.amount().u128().into()) + { + std::cmp::Ordering::Equal => {} + std::cmp::Ordering::Less => { + let diff = remaining_asset + .amount() + .checked_sub(smart_swap.amount().u128().into())?; + + // If the total swap in amount is less than remaining asset, + // adjust the routes to match the remaining asset amount + let largest_route_idx = smart_swap.largest_route_index()?; + + smart_swap.routes[largest_route_idx] + .offer_asset + .add(diff.u128().into())?; + } + std::cmp::Ordering::Greater => { + let diff = smart_swap + .amount() + .checked_sub(remaining_asset.amount().u128().into())?; + + // If the total swap in amount is greater than remaining asset, + // adjust the routes to match the remaining asset amount + let largest_route_idx = smart_swap.largest_route_index()?; + + smart_swap.routes[largest_route_idx].offer_asset.sub(diff)?; + } + } + } + + let user_swap_msg = WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + code_hash: env.contract.code_hash.clone(), + msg: to_binary(&ExecuteMsg::UserSwap { + swap: user_swap, + min_asset: min_asset.clone(), + remaining_asset, + affiliates, + })?, + funds: vec![], + }; + + // Add the user swap message to the response + response = response + .add_message(user_swap_msg) + .add_attribute("action", "dispatch_user_swap"); + + // Create the post swap action message + let post_swap_action_msg = WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + code_hash: env.contract.code_hash.clone(), + msg: to_binary(&ExecuteMsg::PostSwapAction { + min_asset, + timeout_timestamp, + post_swap_action, + exact_out, + })?, + funds: vec![], + }; + + // Add the post swap action message to the response and return the response + Ok(response + .add_message(post_swap_action_msg) + .add_attribute("action", "dispatch_post_swap_action")) +} + +// Entrypoint that catches all errors in SwapAndAction and recovers +// the original funds sent to the contract to a recover address. +#[allow(clippy::too_many_arguments)] +pub fn execute_swap_and_action_with_recover( + deps: DepsMut, + env: Env, + info: MessageInfo, + sent_asset: Option, + user_swap: Swap, + min_asset: Asset, + timeout_timestamp: u64, + post_swap_action: Action, + affiliates: Vec, + recovery_addr: Addr, +) -> ContractResult { + let mut assets: Vec = info.funds.iter().cloned().map(Asset::Native).collect(); + + if let Some(asset) = &sent_asset { + if let Asset::Cw20(_) = asset { + assets.push(asset.clone()); + } + } + + // Store all parameters into a temporary storage. + RECOVER_TEMP_STORAGE.save( + deps.storage, + &RecoverTempStorage { + assets, + recovery_addr, + }, + )?; + + // Then call ExecuteMsg::SwapAndAction using a SubMsg. + let sub_msg = SubMsg::reply_always( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + code_hash: env.contract.code_hash, + msg: to_binary(&ExecuteMsg::SwapAndAction { + sent_asset, + user_swap, + min_asset, + timeout_timestamp, + post_swap_action, + affiliates, + })?, + funds: info.funds, + }), + RECOVER_REPLY_ID, + ); + + Ok(Response::new().add_submessage(sub_msg)) +} + +// Dispatches the user swap and refund/affiliate fee bank sends if needed +pub fn execute_user_swap( + deps: DepsMut, + env: Env, + info: MessageInfo, + swap: Swap, + mut min_asset: Asset, + mut remaining_asset: Asset, + affiliates: Vec, +) -> ContractResult { + // Enforce the caller is the contract itself + if info.sender != env.contract.address { + return Err(ContractError::Unauthorized); + } + + // Create a response object to return + let mut response: Response = Response::new() + .add_attribute("action", "execute_user_swap") + .add_attribute("denom_in", remaining_asset.denom()) + .add_attribute("denom_out", min_asset.denom()); + + // Create affiliate response and total affiliate fee amount + let mut affiliate_response: Response = Response::new(); + let mut total_affiliate_fee_amount: Uint128 = Uint128::zero(); + + // If affiliates exist, create the affiliate fee messages and attributes and + // add them to the affiliate response, updating the total affiliate fee amount + for affiliate in affiliates.iter() { + // Verify, calculate, and get the affiliate fee amount + let affiliate_fee_amount = + verify_and_calculate_affiliate_fee_amount(&deps, &min_asset, affiliate)?; + + if affiliate_fee_amount > Uint128::zero() { + // Add the affiliate fee amount to the total affiliate fee amount + total_affiliate_fee_amount = + total_affiliate_fee_amount.checked_add(affiliate_fee_amount)?; + + // Create the affiliate_fee_asset + let affiliate_fee_asset = Asset::new(deps.api, min_asset.denom(), affiliate_fee_amount); + let affiliate_fee_contract = REGISTERED_TOKENS.load( + deps.storage, + deps.api.addr_validate(affiliate_fee_asset.denom())?, + )?; + + // Create the affiliate fee message + // let affiliate_fee_msg = affiliate_fee_asset.transfer(&affiliate.address); + let affiliate_fee_msg = match snip20::transfer_msg( + affiliate.address.to_string(), + affiliate_fee_asset.amount(), + None, + None, + 0, + affiliate_fee_contract.code_hash.clone(), + affiliate_fee_contract.address.to_string(), + ) { + Ok(msg) => msg, + Err(e) => return Err(ContractError::Std(e)), + }; + + // Add the affiliate fee message and attributes to the response + affiliate_response = affiliate_response + .add_message(affiliate_fee_msg) + .add_attribute("action", "dispatch_affiliate_fee_bank_send") + .add_attribute("address", &affiliate.address) + .add_attribute("amount", affiliate_fee_amount); + } + } + + let remaining_asset_contract = REGISTERED_TOKENS.load( + deps.storage, + deps.api.addr_validate(remaining_asset.denom())?, + )?; + + // Create the user swap message + match swap { + Swap::SwapExactAssetIn(swap) => { + // Validate swap operations + validate_swap_operations(&swap.operations, remaining_asset.denom(), min_asset.denom())?; + + // Get swap adapter contract address from venue name + let user_swap_adapter_contract = + SWAP_VENUE_MAP.load(deps.storage, &swap.swap_venue_name)?; + + // Create the user swap message args + let user_swap_msg_args = SwapHookMsg::Swap { + operations: swap.operations, + }; + + // Create the user swap message + /* + let user_swap_msg = remaining_asset.into_wasm_msg( + user_swap_adapter_contract_address.to_string(), + to_binary(&user_swap_msg_args)?, + )?; + */ + + let user_swap_msg = match snip20::send_msg( + user_swap_adapter_contract.address.to_string(), + remaining_asset.amount(), + Some(to_binary(&user_swap_msg_args)?), + None, + None, + 0, + remaining_asset_contract.code_hash.clone(), + remaining_asset_contract.address.to_string(), + ) { + Ok(msg) => msg, + Err(e) => return Err(ContractError::Std(e)), + }; + + response = response + .add_message(user_swap_msg) + .add_attribute("action", "dispatch_user_swap_exact_asset_in"); + } + Swap::SwapExactAssetOut(swap) => { + // Validate swap operations + validate_swap_operations(&swap.operations, remaining_asset.denom(), min_asset.denom())?; + + // Get swap adapter contract address from venue name + let user_swap_adapter_contract = + SWAP_VENUE_MAP.load(deps.storage, &swap.swap_venue_name)?; + + // Calculate the swap asset out by adding the min asset amount to the total affiliate fee amount + min_asset.add(total_affiliate_fee_amount)?; + + // Query the swap adapter to get the asset in needed to obtain the min asset plus affiliates + let user_swap_asset_in = + query_swap_asset_in(&deps, &user_swap_adapter_contract, &swap, &min_asset)?; + + // Verify the user swap in denom is the same as the denom received from the message to the contract + if user_swap_asset_in.denom() != remaining_asset.denom() { + return Err(ContractError::UserSwapAssetInDenomMismatch); + } + + // Calculate refund amount to send back to the user + remaining_asset.sub(user_swap_asset_in.amount())?; + + // If refund amount gt zero, then create the refund message and add it to the response + if remaining_asset.amount() > Uint128::zero() { + // Get the refund address from the swap + let to_address = swap + .refund_address + .clone() + .ok_or(ContractError::NoRefundAddress)?; + + // Validate the refund address + deps.api.addr_validate(&to_address)?; + + // Get the refund amount + let refund_amount = remaining_asset.amount(); + + let remaining_asset_contract = REGISTERED_TOKENS.load( + deps.storage, + deps.api.addr_validate(remaining_asset.denom())?, + )?; + // Create the refund message + // let refund_msg = remaining_asset.transfer(&to_address); + let refund_msg = match snip20::send_msg( + to_address.to_string(), + remaining_asset.amount(), + None, + None, + None, + 0, + remaining_asset_contract.code_hash.clone(), + remaining_asset_contract.address.to_string(), + ) { + Ok(msg) => msg, + Err(e) => return Err(ContractError::Std(e)), + }; + + // Add the refund message and attributes to the response + response = response + .add_message(refund_msg) + .add_attribute("action", "dispatch_refund") + .add_attribute("address", &to_address) + .add_attribute("amount", refund_amount); + } + + // Create the user swap message args + let user_swap_msg_args = swap; + + // Create the user swap message + /* + let user_swap_msg = user_swap_asset_in.into_wasm_msg( + user_swap_adapter_contract.address.to_string(), + to_binary(&user_swap_msg_args)?, + )?; + */ + let user_swap_msg = match snip20::send_msg( + user_swap_adapter_contract.address.to_string(), + remaining_asset.amount(), + Some(to_binary(&user_swap_msg_args)?), + None, + None, + 0, + remaining_asset_contract.code_hash.clone(), + remaining_asset_contract.address.to_string(), + ) { + Ok(msg) => msg, + Err(e) => return Err(ContractError::Std(e)), + }; + + response = response + .add_message(user_swap_msg) + .add_attribute("action", "dispatch_user_swap_exact_asset_out"); + } + Swap::SmartSwapExactAssetIn(swap) => { + for route in swap.routes { + // Validate swap operations + validate_swap_operations( + &route.operations, + remaining_asset.denom(), + min_asset.denom(), + )?; + + // Get swap adapter contract address from venue name + let user_swap_adapter_contract = + SWAP_VENUE_MAP.load(deps.storage, &swap.swap_venue_name)?; + + // Create the user swap message args + let user_swap_msg_args = SwapHookMsg::Swap { + operations: route.operations, + }; + + // Create the user swap message + /* + let user_swap_msg = route.offer_asset.into_wasm_msg( + user_swap_adapter_contract_address.to_string(), + to_binary(&user_swap_msg_args)?, + )?; + */ + let user_swap_msg = match snip20::send_msg( + user_swap_adapter_contract.address.to_string(), + remaining_asset.amount(), + Some(to_binary(&user_swap_msg_args)?), + None, + None, + 0, + remaining_asset_contract.code_hash.clone(), + remaining_asset_contract.address.to_string(), + ) { + Ok(msg) => msg, + Err(e) => return Err(ContractError::Std(e)), + }; + + response = response + .add_message(user_swap_msg) + .add_attribute("action", "dispatch_user_swap_exact_asset_in"); + } + } + } + + // Add the affiliate messages and attributes to the response and return the response + // Having the affiliate messages after the swap is purposeful, so that the affiliate + // bank sends are valid and the contract has funds to send to the affiliates. + Ok(response + .add_submessages(affiliate_response.messages) + .add_attributes(affiliate_response.attributes)) +} + +// Dispatches the post swap action +// Can only be called by the contract itself +pub fn execute_post_swap_action( + deps: DepsMut, + env: Env, + info: MessageInfo, + min_asset: Asset, + timeout_timestamp: u64, + post_swap_action: Action, + exact_out: bool, +) -> ContractResult { + // Enforce the caller is the contract itself + if info.sender != env.contract.address { + return Err(ContractError::Unauthorized); + } + + // Create a response object to return + let mut response: Response = + Response::new().add_attribute("action", "execute_post_swap_action"); + + // Get the pre swap out asset amount from storage + let pre_swap_out_asset_amount = PRE_SWAP_OUT_ASSET_AMOUNT.load(deps.storage)?; + + // Get contract balance of min out asset post swap + // for fee deduction and transfer out amount enforcement + // let post_swap_out_asset = get_current_asset_available(&deps, &env, min_asset.denom())?; + let min_asset_contract = + REGISTERED_TOKENS.load(deps.storage, deps.api.addr_validate(min_asset.denom())?)?; + let viewing_key = VIEWING_KEY.load(deps.storage)?; + let post_swap_out_asset_amount = match snip20::balance_query( + deps.querier, + env.contract.address.to_string(), + viewing_key, + 0, + min_asset_contract.code_hash.clone(), + min_asset_contract.address.to_string(), + ) { + Ok(balance) => balance.amount, + Err(e) => return Err(ContractError::Std(e)), + }; + + // Set the transfer out asset to the post swap out asset amount minus the pre swap out asset amount + // Since we only want to transfer out the amount received from the swap + let transfer_out_asset = Asset::new( + deps.api, + min_asset.denom(), + post_swap_out_asset_amount - pre_swap_out_asset_amount, + ); + + // Error if the contract balance is less than the min asset out amount + if transfer_out_asset.amount() < min_asset.amount() { + return Err(ContractError::ReceivedLessAssetFromSwapsThanMinAsset); + } + + // Set the transfer out asset to the min asset if exact out is true + let transfer_out_asset = if exact_out { + min_asset + } else { + transfer_out_asset + }; + + response = response + .add_attribute( + "post_swap_action_amount_out", + transfer_out_asset.amount().to_string(), + ) + .add_attribute("post_swap_action_denom_out", transfer_out_asset.denom()); + + // Dispatch the action message + response = validate_and_dispatch_action( + deps, + post_swap_action, + transfer_out_asset, + timeout_timestamp, + response, + )?; + + Ok(response) +} + +// Dispatches an action +#[allow(clippy::too_many_arguments)] +pub fn execute_action( + deps: DepsMut, + env: Env, + _info: MessageInfo, + sent_asset: Option, + timeout_timestamp: u64, + action: Action, + exact_out: bool, + min_asset: Option, +) -> ContractResult { + // Create a response object to return + let mut response: Response = Response::new().add_attribute("action", "execute_action"); + + // Validate and unwrap the sent asset + let sent_asset = match sent_asset { + Some(sent_asset) => { + // sent_asset.validate(&deps, &env, &info)?; + // TODO validate + sent_asset + } + None => { + return Err(ContractError::NativeCoinNotSupported); + } + }; + + // Error if the current block time is greater than the timeout timestamp + if env.block.time.nanos() > timeout_timestamp { + return Err(ContractError::Timeout); + } + + // Already validated at entrypoints (both direct and snip20_receive) + let mut remaining_asset = sent_asset; + + // If the post swap action is an IBC transfer, then handle the ibc fees + // by either creating a fee swap message or deducting the ibc fees from + // the remaining asset received amount. + if let Action::IbcTransfer { ibc_info, fee_swap } = &action { + response = + handle_ibc_transfer_fees(&deps, ibc_info, fee_swap, &mut remaining_asset, response)?; + } + + // Validate and determine the asset to be used for the action + let action_asset = if exact_out { + let min_asset = min_asset.ok_or(ContractError::NoMinAssetProvided)?; + + // Ensure remaining_asset and min_asset have the same denom + if remaining_asset.denom() != min_asset.denom() { + return Err(ContractError::ActionDenomMismatch); + } + + // Ensure remaining_asset is greater than or equal to min_asset + if remaining_asset.amount() < min_asset.amount() { + return Err(ContractError::RemainingAssetLessThanMinAsset); + } + + min_asset + } else { + remaining_asset.clone() + }; + + // Dispatch the action message + response = + validate_and_dispatch_action(deps, action, action_asset, timeout_timestamp, response)?; + + // Return the response + Ok(response) +} + +// Entrypoint that catches all errors in Action and recovers +// the original funds sent to the contract to a recover address. +#[allow(clippy::too_many_arguments)] +pub fn execute_action_with_recover( + deps: DepsMut, + env: Env, + info: MessageInfo, + sent_asset: Option, + timeout_timestamp: u64, + action: Action, + exact_out: bool, + min_asset: Option, + recovery_addr: Addr, +) -> ContractResult { + let mut assets: Vec = info.funds.iter().cloned().map(Asset::Native).collect(); + + if let Some(asset) = &sent_asset { + if let Asset::Cw20(_) = asset { + assets.push(asset.clone()); + } + } + + // Store all parameters into a temporary storage. + RECOVER_TEMP_STORAGE.save( + deps.storage, + &RecoverTempStorage { + assets, + recovery_addr, + }, + )?; + + // Then call ExecuteMsg::Action using a SubMsg. + let sub_msg = SubMsg::reply_always( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + code_hash: env.contract.code_hash.to_string(), + msg: to_binary(&ExecuteMsg::Action { + sent_asset, + timeout_timestamp, + action, + exact_out, + min_asset, + })?, + funds: info.funds, + }), + RECOVER_REPLY_ID, + ); + + Ok(Response::new().add_submessage(sub_msg)) +} + +//////////////////////// +/// HELPER FUNCTIONS /// +//////////////////////// + +// ACTION HELPER FUNCTIONS + +// Validates and adds an action message to the response +fn validate_and_dispatch_action( + deps: DepsMut, + action: Action, + action_asset: Asset, + timeout_timestamp: u64, + mut response: Response, +) -> Result { + match action { + Action::Transfer { to_address } => { + // Error if the destination address is not a valid address on the current chain + deps.api.addr_validate(&to_address)?; + + let action_asset_contract = REGISTERED_TOKENS + .load(deps.storage, deps.api.addr_validate(action_asset.denom())?)?; + // Create the transfer message + let transfer_msg = match snip20::send_msg( + to_address.to_string(), + action_asset.amount(), + None, + None, + None, + 0, + action_asset_contract.code_hash.clone(), + action_asset_contract.address.to_string(), + ) { + Ok(msg) => msg, + Err(e) => return Err(ContractError::Std(e)), + }; + + // Add the transfer message to the response + response = response + .add_message(transfer_msg) + .add_attribute("action", "dispatch_action_transfer"); + } + Action::IbcTransfer { ibc_info, .. } => { + // Validates recover address, errors if invalid + deps.api.addr_validate(&ibc_info.recover_address)?; + + let transfer_out_contract = match action_asset { + Asset::Native(_) => { + return Err(ContractError::NativeCoinNotSupported); + } + _ => REGISTERED_TOKENS + .load(deps.storage, deps.api.addr_validate(action_asset.denom())?)?, + }; + + // Get the IBC transfer adapter contract address + let ibc_transfer_contract = IBC_TRANSFER_CONTRACT.load(deps.storage)?; + + // Send the IBC transfer by calling the IBC transfer contract + let ibc_transfer_msg = match snip20::send_msg_with_code_hash( + ibc_transfer_contract.address.to_string(), + Some(ibc_transfer_contract.code_hash), + action_asset.amount(), + Some(to_binary(&ibc::Snip20HookMsg::IbcTransfer { + info: ibc_info, + timeout_timestamp, + })?), + None, + None, + 0, + transfer_out_contract.code_hash.clone(), + transfer_out_contract.address.to_string(), + ) { + Ok(msg) => match msg { + CosmosMsg::Wasm(wasm_msg) => wasm_msg, + _ => return Err(ContractError::Std(StdError::generic_err("Invalid WasmMsg"))), + }, + Err(e) => return Err(ContractError::Std(e)), + }; + + // Add the IBC transfer message to the response + response = response + .add_message(ibc_transfer_msg) + .add_attribute("action", "dispatch_action_ibc_transfer"); + } + /* + Action::ContractCall { + contract_address, + msg, + } => { + // Verify the contract address is valid, error if invalid + let checked_contract_address = deps.api.addr_validate(&contract_address)?; + + // Error if the contract address is in the blocked contract addresses map + if BLOCKED_CONTRACT_ADDRESSES.has(deps.storage, &checked_contract_address) { + return Err(ContractError::ContractCallAddressBlocked); + } + + let action_asset_contract = REGISTERED_TOKENS + .load(deps.storage, deps.api.addr_validate(action_asset.denom())?)?; + + // Create the contract call message + let contract_call_msg = WasmMsg::Execute { + contract_addr: action_asset_contract.address.to_string(), + code_hash: action_asset_contract.code_hash, + msg: to_binary(&msg)?, + funds: vec![], + }; + + // Add the contract call message to the response + response = response + .add_message(contract_call_msg) + .add_attribute("action", "dispatch_action_contract_call"); + } + Action::HplTransfer { + dest_domain, + recipient, + hook, + metadata, + warp_address, + } => { + let transfer_out_coin = match action_asset { + Asset::Native(coin) => coin, + _ => return Err(ContractError::NonNativeHplTransfer), + }; + + // Create the Hyperlane transfer message + let hpl_transfer_msg: HplExecuteMsg = HplTransfer { + dest_domain, + recipient, + hook, + metadata, + warp_address, + }; + + // Get the Hyperlane transfer adapter contract address + let hpl_transfer_contract = HYPERLANE_TRANSFER_CONTRACT_ADDRESS.load(deps.storage)?; + + // Send the Hyperlane transfer by calling the Hyperlane transfer contract + let hpl_transfer_msg = WasmMsg::Execute { + contract_addr: hpl_transfer_contract.address.to_string(), + code_hash: hpl_transfer_contract.code_hash, + msg: to_binary(&hpl_transfer_msg)?, + funds: vec![transfer_out_coin], + }; + + // Add the Hyperlane transfer message to the response + response = response + .add_message(hpl_transfer_msg) + .add_attribute("action", "dispatch_action_ibc_transfer"); + } + */ + _ => { + return Err(ContractError::UnsupportedAction); + } + }; + + Ok(response) +} + +// IBC FEE HELPER FUNCTIONS + +// Creates the fee swap and ibc transfer messages and adds them to the response +fn handle_ibc_transfer_fees( + deps: &DepsMut, + ibc_info: &IbcInfo, + fee_swap: &Option, + remaining_asset: &mut Asset, + mut response: Response, +) -> Result { + let ibc_fee_coin = ibc_info + .fee + .as_ref() + .map(|fee| fee.one_coin()) + .transpose()?; + + if let Some(fee_swap) = fee_swap { + // NOTE unsure if this works + let ibc_fee_coin = ibc_fee_coin + .clone() + .ok_or(ContractError::FeeSwapWithoutIbcFees)?; + + // NOTE: this call mutates remaining_asset by deducting ibc_fee_coin's amount from it + let fee_swap_msg = + verify_and_create_fee_swap_msg(deps, fee_swap, remaining_asset, &ibc_fee_coin)?; + + // Add the fee swap message to the response + response = response + .add_message(fee_swap_msg) + .add_attribute("action", "dispatch_fee_swap"); + } else if let Some(ibc_fee_coin) = &ibc_fee_coin { + if remaining_asset.denom() != ibc_fee_coin.denom { + return Err(ContractError::IBCFeeDenomDiffersFromAssetReceived); + } + + // Deduct the ibc_fee_coin amount from the remaining asset amount + remaining_asset.sub(ibc_fee_coin.amount)?; + } + + // Dispatch the ibc fee bank send to the ibc transfer adapter contract if needed + if let Some(ibc_fee_coin) = ibc_fee_coin { + // Get the ibc transfer adapter contract address + let ibc_transfer_contract = IBC_TRANSFER_CONTRACT.load(deps.storage)?; + + // Create the ibc fee bank send message + let ibc_fee_msg = BankMsg::Send { + to_address: ibc_transfer_contract.address.to_string(), + amount: vec![ibc_fee_coin], + }; + + // Add the ibc fee message to the response + response = response + .add_message(ibc_fee_msg) + .add_attribute("action", "dispatch_ibc_fee_bank_send"); + } + + Ok(response) +} + +// SWAP MESSAGE HELPER FUNCTIONS + +// Creates the fee swap message and returns it +// Also deducts the fee swap in amount from the mutable remaining asset +fn verify_and_create_fee_swap_msg( + deps: &DepsMut, + fee_swap: &SwapExactAssetOut, + remaining_asset: &mut Asset, + ibc_fee_coin: &Coin, +) -> ContractResult { + // Validate swap operations + validate_swap_operations( + &fee_swap.operations, + remaining_asset.denom(), + &ibc_fee_coin.denom, + )?; + + // Get swap adapter contract address from venue name + let fee_swap_adapter_contract = SWAP_VENUE_MAP.load(deps.storage, &fee_swap.swap_venue_name)?; + + // Query the swap adapter to get the asset in needed for the fee swap + let fee_swap_asset_in = query_swap_asset_in( + deps, + &fee_swap_adapter_contract, + fee_swap, + &ibc_fee_coin.clone().into(), + )?; + + // Verify the fee swap in denom is the same as the denom received from the message to the contract + if fee_swap_asset_in.denom() != remaining_asset.denom() { + return Err(ContractError::FeeSwapAssetInDenomMismatch); + } + + // Deduct the fee swap in amount from the remaining asset amount + // Error if swap requires more than the remaining asset amount + remaining_asset.sub(fee_swap_asset_in.amount())?; + + // Create the fee swap message args + let fee_swap_msg_args = fee_swap.clone(); + + let fee_swap_asset_contract = REGISTERED_TOKENS.load( + deps.storage, + deps.api.addr_validate(fee_swap_asset_in.denom())?, + )?; + + // Create the fee swap message + /* + let fee_swap_msg = fee_swap_asset_in.into_wasm_msg( + fee_swap_adapter_contract.address.to_string(), + to_binary(&fee_swap_msg_args)?, + )?; + */ + let fee_swap_msg = match snip20::send_msg( + fee_swap_adapter_contract.address.to_string(), + remaining_asset.amount(), + Some(to_binary(&fee_swap_msg_args)?), + None, + None, + 0, + fee_swap_asset_contract.code_hash.clone(), + fee_swap_asset_contract.address.to_string(), + ) { + Ok(msg) => match msg { + CosmosMsg::Wasm(wasm_msg) => wasm_msg, + _ => return Err(ContractError::Std(StdError::generic_err("Invalid WasmMsg"))), + }, + Err(e) => return Err(ContractError::Std(e)), + }; + + Ok(fee_swap_msg) +} + +// AFFILIATE FEE HELPER FUNCTIONS + +// Verifies the affiliate address is valid, if so then +// returns the calculated affiliate fee amount. +fn verify_and_calculate_affiliate_fee_amount( + deps: &DepsMut, + min_asset: &Asset, + affiliate: &Affiliate, +) -> ContractResult { + // Verify the affiliate address is valid + deps.api.addr_validate(&affiliate.address)?; + + // Get the affiliate fee amount by multiplying the min_asset + // amount by the affiliate basis points fee divided by 10000 + let affiliate_fee_amount = min_asset + .amount() + .multiply_ratio(affiliate.basis_points_fee, Uint128::new(10000)); + + Ok(affiliate_fee_amount) +} + +// QUERY HELPER FUNCTIONS + +// Unexposed query helper function that queries the swap adapter contract to get the +// asset in needed for a given swap. Verifies the swap's in denom is the same as the +// swap asset denom from the message. Returns the swap asset in. +fn query_swap_asset_in( + deps: &DepsMut, + swap_adapter_contract: &ContractInfo, + swap: &SwapExactAssetOut, + swap_asset_out: &Asset, +) -> ContractResult { + // Query the swap adapter to get the asset in needed for the fee swap + let fee_swap_asset_in: Asset = deps.querier.query_wasm_smart( + swap_adapter_contract.address.clone(), + swap_adapter_contract.code_hash.clone(), + &SwapQueryMsg::SimulateSwapExactAssetIn { + asset_in: swap_asset_out.clone(), + swap_operations: swap.operations.clone(), + }, + )?; + + Ok(fee_swap_asset_in) +} diff --git a/contracts/secret-entry-point/src/hyperlane.rs b/contracts/secret-entry-point/src/hyperlane.rs new file mode 100644 index 00000000..65506a5b --- /dev/null +++ b/contracts/secret-entry-point/src/hyperlane.rs @@ -0,0 +1,37 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::HexBinary; + +/////////////// +/// MIGRATE /// +/////////////// + +// The MigrateMsg struct defines the migration parameters used. +#[cw_serde] +pub struct MigrateMsg { + pub entry_point_contract_address: String, +} +/////////////////// +/// INSTANTIATE /// +/////////////////// + +// The InstantiateMsg struct defines the initialization parameters for the IBC Transfer Adapter contracts. +#[cw_serde] +pub struct InstantiateMsg { + pub entry_point_contract_address: String, +} + +/////////////// +/// EXECUTE /// +/////////////// + +// The ExecuteMsg enum defines the execution message that the IBC Transfer Adapter contracts can handle. +#[cw_serde] +pub enum ExecuteMsg { + HplTransfer { + dest_domain: u32, + recipient: HexBinary, + hook: Option, + metadata: Option, + warp_address: String, + }, +} diff --git a/contracts/secret-entry-point/src/lib.rs b/contracts/secret-entry-point/src/lib.rs new file mode 100644 index 00000000..31ee4b92 --- /dev/null +++ b/contracts/secret-entry-point/src/lib.rs @@ -0,0 +1,8 @@ +pub mod contract; +pub mod error; +pub mod execute; +pub mod hyperlane; +pub mod msg; +pub mod query; +pub mod reply; +pub mod state; diff --git a/contracts/secret-entry-point/src/msg.rs b/contracts/secret-entry-point/src/msg.rs new file mode 100644 index 00000000..20383ae2 --- /dev/null +++ b/contracts/secret-entry-point/src/msg.rs @@ -0,0 +1,182 @@ +use secret_skip::{ + asset::Asset, + ibc::IbcInfo, + snip20::Snip20ReceiveMsg, + swap::{Swap, SwapExactAssetOut}, +}; + +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Binary, ContractInfo, HexBinary, Uint128}; + +#[cw_serde] +pub struct SwapVenue { + pub name: String, + pub adapter_contract: ContractInfo, +} + +/////////////// +/// MIGRATE /// +/////////////// + +// The MigrateMsg struct defines the migration parameters for the entry point contract. +#[cw_serde] +pub struct MigrateMsg {} + +/////////////////// +/// INSTANTIATE /// +/////////////////// + +// The InstantiateMsg struct defines the initialization parameters for the entry point contract. +#[cw_serde] +pub struct InstantiateMsg { + pub swap_venues: Vec, + pub ibc_transfer_contract: ContractInfo, + // pub hyperlane_transfer_contract: Option, + pub viewing_key: String, +} + +/////////////// +/// EXECUTE /// +/////////////// + +// The ExecuteMsg enum defines the execution messages that the entry point contract can handle. +// Only the SwapAndAction message is callable by external users. +#[cw_serde] +#[allow(clippy::large_enum_variant)] +pub enum ExecuteMsg { + RegisterTokens { + contracts: Vec, + }, + Receive(Snip20ReceiveMsg), + SwapAndActionWithRecover { + sent_asset: Option, + user_swap: Swap, + min_asset: Asset, + timeout_timestamp: u64, + post_swap_action: Action, + affiliates: Vec, + recovery_addr: Addr, + }, + SwapAndAction { + sent_asset: Option, + user_swap: Swap, + min_asset: Asset, + timeout_timestamp: u64, + post_swap_action: Action, + affiliates: Vec, + }, + UserSwap { + swap: Swap, + min_asset: Asset, + remaining_asset: Asset, + affiliates: Vec, + }, + PostSwapAction { + min_asset: Asset, + timeout_timestamp: u64, + post_swap_action: Action, + exact_out: bool, + }, + Action { + sent_asset: Option, + timeout_timestamp: u64, + action: Action, + exact_out: bool, + min_asset: Option, + }, + ActionWithRecover { + sent_asset: Option, + timeout_timestamp: u64, + action: Action, + exact_out: bool, + min_asset: Option, + recovery_addr: Addr, + }, +} + +/// This structure describes a CW20 hook message. +#[cw_serde] +pub enum Snip20HookMsg { + SwapAndActionWithRecover { + user_swap: Swap, + min_asset: Asset, + timeout_timestamp: u64, + post_swap_action: Action, + affiliates: Vec, + recovery_addr: Addr, + }, + SwapAndAction { + user_swap: Swap, + min_asset: Asset, + timeout_timestamp: u64, + post_swap_action: Action, + affiliates: Vec, + }, + Action { + timeout_timestamp: u64, + action: Action, + exact_out: bool, + min_asset: Option, + }, + ActionWithRecover { + timeout_timestamp: u64, + action: Action, + exact_out: bool, + min_asset: Option, + recovery_addr: Addr, + }, +} + +///////////// +/// QUERY /// +///////////// + +// The QueryMsg enum defines the queries the entry point contract provides. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + // SwapVenueAdapterContract returns the address of the swap + // adapter contract for the given swap venue name. + #[returns(cosmwasm_std::Addr)] + SwapVenueAdapterContract { name: String }, + + // IbcTransferAdapterContract returns the address of the IBC + // transfer adapter contract. + #[returns(cosmwasm_std::Addr)] + IbcTransferAdapterContract {}, +} + +//////////////////// +/// COMMON TYPES /// +//////////////////// + +// The Action enum is used to specify what action to take after a swap. +#[cw_serde] +pub enum Action { + Transfer { + to_address: String, + }, + IbcTransfer { + ibc_info: IbcInfo, + fee_swap: Option, + }, + ContractCall { + contract_address: String, + msg: Binary, + }, + HplTransfer { + dest_domain: u32, + recipient: HexBinary, + hook: Option, + metadata: Option, + warp_address: String, + }, +} + +// The Affiliate struct is used to specify an affiliate address and BPS fee taken +// from the min_asset to send to that address. +#[cw_serde] +pub struct Affiliate { + pub basis_points_fee: Uint128, + pub address: String, +} diff --git a/contracts/secret-entry-point/src/query.rs b/contracts/secret-entry-point/src/query.rs new file mode 100644 index 00000000..7842aac2 --- /dev/null +++ b/contracts/secret-entry-point/src/query.rs @@ -0,0 +1,12 @@ +use crate::state::{IBC_TRANSFER_CONTRACT, SWAP_VENUE_MAP}; +use cosmwasm_std::{Addr, Deps, StdResult}; + +// Queries the swap venue map by name and returns the swap adapter contract address if it exists +pub fn query_swap_venue_adapter_contract(deps: Deps, name: String) -> StdResult { + Ok(SWAP_VENUE_MAP.load(deps.storage, &name)?.address) +} + +// Queries the IBC transfer adapter contract address and returns it if it exists +pub fn query_ibc_transfer_adapter_contract(deps: Deps) -> StdResult { + Ok(IBC_TRANSFER_CONTRACT.load(deps.storage)?.address) +} diff --git a/contracts/secret-entry-point/src/reply.rs b/contracts/secret-entry-point/src/reply.rs new file mode 100644 index 00000000..3c547f78 --- /dev/null +++ b/contracts/secret-entry-point/src/reply.rs @@ -0,0 +1,58 @@ +use crate::{ + error::ContractError, + state::{RECOVER_TEMP_STORAGE, REGISTERED_TOKENS}, +}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, CosmosMsg, DepsMut, Reply, Response, SubMsgResult}; +use secret_skip::asset::Asset; +use secret_toolkit::snip20; + +pub const RECOVER_REPLY_ID: u64 = 1; + +#[cw_serde] +pub struct RecoverTempStorage { + pub assets: Vec, + pub recovery_addr: Addr, +} + +pub fn reply_swap_and_action_with_recover( + deps: DepsMut, + msg: Reply, +) -> Result { + match msg.result { + SubMsgResult::Ok(_response) => { + RECOVER_TEMP_STORAGE.remove(deps.storage); + + Ok(Response::new().add_attribute("status", "swap_and_action_successful")) + } + SubMsgResult::Err(e) => { + let storage = RECOVER_TEMP_STORAGE.load(deps.storage)?; + + let mut return_assets_msgs: Vec = vec![]; + + for return_asset in storage.assets.into_iter() { + let return_asset_contract = REGISTERED_TOKENS + .load(deps.storage, deps.api.addr_validate(return_asset.denom())?)?; + match snip20::transfer_msg( + storage.recovery_addr.to_string(), + return_asset.amount(), + None, + None, + 0, + return_asset_contract.code_hash.clone(), + return_asset_contract.address.to_string(), + ) { + Ok(msg) => return_assets_msgs.push(msg), + Err(e) => return Err(ContractError::Std(e)), + }; + } + + RECOVER_TEMP_STORAGE.remove(deps.storage); + + Ok(Response::new() + .add_messages(return_assets_msgs) + .add_attribute("status", "swap_and_action_failed") + .add_attribute("error", e)) + } + } +} diff --git a/contracts/secret-entry-point/src/state.rs b/contracts/secret-entry-point/src/state.rs new file mode 100644 index 00000000..df07d625 --- /dev/null +++ b/contracts/secret-entry-point/src/state.rs @@ -0,0 +1,26 @@ +use crate::reply::RecoverTempStorage; +use cosmwasm_std::{Addr, ContractInfo, Uint128}; +use secret_storage_plus::{Item, Map}; + +pub const BLOCKED_CONTRACT_ADDRESSES: Map<&Addr, ()> = Map::new("blocked_contract_addresses"); +pub const SWAP_VENUE_MAP: Map<&str, ContractInfo> = Map::new("swap_venue_map"); +pub const IBC_TRANSFER_CONTRACT: Item = Item::new("ibc_transfer_contract"); +/* +pub const HYPERLANE_TRANSFER_CONTRACT_ADDRESS: Item = + Item::new("hyperlane_transfer_contract_address"); +*/ + +// Temporary state to save variables to be used in +// reply handling in case of recovering from an error +pub const RECOVER_TEMP_STORAGE: Item = Item::new("recover_temp_storage"); + +// Temporary state to save the amount of the out asset the contract +// has pre swap so that we can ensure the amount transferred out does not +// exceed the amount the contract obtained from the current swap/call +pub const PRE_SWAP_OUT_ASSET_AMOUNT: Item = Item::new("pre_swap_out_asset_amount"); + +// Secret Network tokens need to be registered for viewing key setup +// and storing contract code hash +pub const REGISTERED_TOKENS: Map = Map::new("registered_tokens"); + +pub const VIEWING_KEY: Item = Item::new("viewing_key"); diff --git a/contracts/secret-entry-point/tests/test_execute_action.rs b/contracts/secret-entry-point/tests/test_execute_action.rs new file mode 100644 index 00000000..feff21e6 --- /dev/null +++ b/contracts/secret-entry-point/tests/test_execute_action.rs @@ -0,0 +1,494 @@ +use cosmwasm_std::{ + testing::{mock_dependencies_with_balances, mock_env, mock_info}, + to_binary, Addr, BankMsg, Coin, ContractInfo, ContractResult, QuerierResult, + ReplyOn::Never, + SubMsg, SystemResult, Timestamp, Uint128, WasmMsg, WasmQuery, +}; +use secret_skip::{ + asset::Asset, + cw20::Cw20Coin, + ibc::{self, ExecuteMsg as IbcTransferExecuteMsg, IbcFee, IbcInfo, Ics20TransferMsg}, + snip20, +}; +//use secret_toolkit::snip20; +use skip_go_secret_entry_point::{ + error::ContractError, + msg::{Action, ExecuteMsg}, + state::{BLOCKED_CONTRACT_ADDRESSES, IBC_TRANSFER_CONTRACT, REGISTERED_TOKENS}, +}; +use test_case::test_case; + +/* +Test Cases: + +Expect Response + // General + - Native Asset Transfer + - Cw20 Asset Transfer + - Ibc Transfer + - Native Asset Contract Call + - Cw20 Asset Contract Call + + // Exact Out + - Ibc Transfer With Exact Out Set To True + - Ibc Transfer w/ IBC Fees of same denom as min coin With Exact Out Set To True + +Expect Error + - Remaining Asset Less Than Min Asset - Native + - Remaining Asset Less Than Min Asset - CW20 + - Contract Call Address Blocked + - Ibc Transfer w/ IBC Fees of different denom than min coin no fee swap + */ + +// Define test parameters +struct Params { + info_funds: Vec, + sent_asset: Option, + action: Action, + exact_out: bool, + min_asset: Option, + expected_messages: Vec, + expected_error: Option, +} + +// Test execute_action +#[test_case( + Params { + info_funds: vec![Coin::new(1_000_000, "os")], + sent_asset: Some(Asset::Cw20(Cw20Coin { + address: "secret123".to_string(), + amount: 1_000_000u128.into(), + })), + min_asset: None, + action: Action::Transfer { + to_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5".to_string(), + }, + exact_out: false, + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "secret123".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&snip20::ExecuteMsg::Send { + amount: 1_000_000u128.into(), + recipient: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5".to_string(), + recipient_code_hash: None, + memo: None, + padding: None, + msg: None, + }).unwrap(), + funds: vec![], + }.into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Cw20 Asset Transfer")] +#[test_case( + Params { + info_funds: vec![Coin::new(1_000_000, "os")], + sent_asset: Some(Asset::Cw20(Cw20Coin { + address: "secret123".to_string(), + amount: 1_000_000u128.into(), + })), + min_asset: None, + action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: None, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5".to_string(), + }, + fee_swap: None, + }, + exact_out: false, + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "secret123".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&snip20::ExecuteMsg::Send { + amount: 1_000_000u128.into(), + recipient: "ibc_transfer_adapter".to_string(), + recipient_code_hash: Some("code_hash".to_string()), + memo: None, + padding: None, + msg: Some(to_binary(&ibc::Snip20HookMsg::IbcTransfer { + info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: None, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5".to_string(), + }, + timeout_timestamp: 101, + }).unwrap()), + }).unwrap(), + funds: vec![], + }.into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Ibc Transfer")] +/* +#[test_case( + Params { + info_funds: vec![], + sent_asset: Some(Asset::Cw20(Cw20Coin{ + address: "secret123".to_string(), + amount: 1_000_000u128.into(), + })), + min_asset: None, + action: Action::ContractCall { + contract_address: "contract_call".to_string(), + msg: to_binary(&"contract_call_msg").unwrap(), + }, + exact_out: false, + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "secret123".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&Cw20ExecuteMsg::Send { + contract: "contract_call".to_string(), + amount: Uint128::new(1_000_000), + msg: to_binary(&"contract_call_msg").unwrap(), + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Cw20 Asset Contract Call")] +*/ +#[test_case( + Params { + info_funds: vec![Coin::new(1_000_000, "os")], + sent_asset: Some(Asset::Cw20(Cw20Coin { + address: "secret123".to_string(), + amount: 1_000_000u128.into(), + })), + min_asset: Some(Asset::Cw20(Cw20Coin { + address: "secret123".to_string(), + amount: 900_000u128.into(), + })), + action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: None, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: None, + }, + exact_out: true, + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "secret123".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&snip20::ExecuteMsg::Send { + amount: 900_000u128.into(), + recipient: "ibc_transfer_adapter".to_string(), + recipient_code_hash: Some("code_hash".to_string()), + memo: None, + padding: None, + msg: Some(to_binary(&ibc::Snip20HookMsg::IbcTransfer { + info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: None, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5".to_string(), + }, + timeout_timestamp: 101, + }).unwrap()), + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Ibc Transfer With Exact Out Set To True")] +/* +#[test_case( + Params { + info_funds: vec![Coin::new(1_200_000, "os")], + sent_asset: Some(Asset::Native(Coin::new(1_200_000, "os"))), + min_asset: Some(Asset::Native(Coin::new(900_000, "os"))), + action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "os")], + timeout_fee: vec![Coin::new(100_000, "os")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: None, + }, + exact_out: true, + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "ibc_transfer_adapter".to_string(), + amount: vec![Coin::new(200_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "ibc_transfer_adapter".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&IbcTransferExecuteMsg::IbcTransfer { + info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "os")], + timeout_fee: vec![Coin::new(100_000, "os")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + coin: Coin::new(900_000, "os"), + timeout_timestamp: 101, + }) + .unwrap(), + funds: vec![Coin::new(900_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Ibc Transfer w/ IBC Fees of same denom as min coin With Exact Out Set To True")] +#[test_case( + Params { + info_funds: vec![Coin::new(1_000_000, "os")], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "os"))), + min_asset: Some(Asset::Native(Coin::new(900_000, "os"))), + action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "un")], + timeout_fee: vec![Coin::new(100_000, "un")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: None, + }, + exact_out: true, + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "ibc_transfer_adapter".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&IbcTransferExecuteMsg::IbcTransfer { + info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "un")], + timeout_fee: vec![Coin::new(100_000, "un")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + coin: Coin::new(900_000, "os"), + timeout_timestamp: 101, + }) + .unwrap(), + funds: vec![Coin::new(900_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: Some(ContractError::IBCFeeDenomDiffersFromAssetReceived), + }; + "Ibc Transfer w/ IBC Fees of different denom than min coin no fee swap - Expect Error")] +#[test_case( + Params { + info_funds: vec![Coin::new(1_000_000, "os")], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "os"))), + min_asset: Some(Asset::Native(Coin::new(1_100_000, "os"))), + action: Action::ContractCall { + contract_address: "entry_point".to_string(), + msg: to_binary(&"contract_call_msg").unwrap(), + }, + exact_out: true, + expected_messages: vec![], + expected_error: Some(ContractError::RemainingAssetLessThanMinAsset), + }; + "Remaining Asset Less Than Min Asset Native - Expect Error")] +#[test_case( + Params { + info_funds: vec![], + sent_asset: Some(Asset::Cw20(Cw20Coin{ + address: "secret123".to_string(), + amount: Uint128::new(1_000_000), + })), + min_asset: Some(Asset::Cw20(Cw20Coin{ + address: "secret123".to_string(), + amount: Uint128::new(2_100_000), + })), + action: Action::ContractCall { + contract_address: "entry_point".to_string(), + msg: to_binary(&"contract_call_msg").unwrap(), + }, + exact_out: true, + expected_messages: vec![], + expected_error: Some(ContractError::RemainingAssetLessThanMinAsset), + }; + "Remaining Asset Less Than Min Asset CW20 - Expect Error")] +#[test_case( + Params { + info_funds: vec![Coin::new(1_000_000, "os")], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "os"))), + min_asset: None, + action: Action::ContractCall { + contract_address: "entry_point".to_string(), + msg: to_binary(&"contract_call_msg").unwrap(), + }, + exact_out: false, + expected_messages: vec![], + expected_error: Some(ContractError::ContractCallAddressBlocked), + }; + "Contract Call Address Blocked - Expect Error")] +*/ +fn test_execute_post_swap_action(params: Params) { + // Create mock dependencies + let mut deps = mock_dependencies_with_balances(&[("entry_point", &[])]); + + // Create mock wasm handler to handle the swap adapter contract query + let wasm_handler = |query: &WasmQuery| -> QuerierResult { + match query { + WasmQuery::Smart { .. } => SystemResult::Ok(ContractResult::Ok( + to_binary(&snip20::BalanceResponse { + amount: 1_000_000u128.into(), + }) + .unwrap(), + )), + _ => panic!("Unsupported query: {:?}", query), + } + }; + + // Update querier with mock wasm handler + deps.querier.update_wasm(wasm_handler); + + // Create mock env with parameters that make testing easier + let mut env = mock_env(); + env.contract.address = Addr::unchecked("entry_point"); + env.contract.code_hash = "code_hash".to_string(); + env.block.time = Timestamp::from_nanos(100); + + // Convert info funds vector into a slice of Coin objects + let info_funds: &[Coin] = ¶ms.info_funds; + + // Create mock info with entry point contract address + let info = mock_info("actioner", info_funds); + + // Store the ibc transfer adapter contract address + let ibc_transfer_adapter = ContractInfo { + address: Addr::unchecked("ibc_transfer_adapter"), + code_hash: "code_hash".to_string(), + }; + IBC_TRANSFER_CONTRACT + .save(deps.as_mut().storage, &ibc_transfer_adapter) + .unwrap(); + + REGISTERED_TOKENS + .save( + deps.as_mut().storage, + Addr::unchecked("secret123"), + &ContractInfo { + address: Addr::unchecked("secret123"), + code_hash: "code_hash".to_string(), + }, + ) + .unwrap(); + + // Store the entry point contract address in the blocked contract addresses map + BLOCKED_CONTRACT_ADDRESSES + .save(deps.as_mut().storage, &Addr::unchecked("entry_point"), &()) + .unwrap(); + + // Call execute_post_swap_action with the given test parameters + let res = skip_go_secret_entry_point::contract::execute( + deps.as_mut(), + env, + info, + ExecuteMsg::Action { + sent_asset: params.sent_asset, + timeout_timestamp: 101, + action: params.action, + exact_out: params.exact_out, + min_asset: params.min_asset, + }, + ); + + match res { + Ok(res) => { + // Assert the test did not expect an error + assert!( + params.expected_error.is_none(), + "expected test to error with {:?}, but it succeeded", + params.expected_error + ); + + // Assert the number of messages in the response is correct + assert_eq!( + res.messages.len(), + params.expected_messages.len(), + "expected {:?} messages, but got {:?}", + params.expected_messages.len(), + res.messages.len() + ); + + // Assert the messages in the response are correct + assert_eq!(res.messages, params.expected_messages,); + } + Err(err) => { + // Assert the test expected an error + assert!( + params.expected_error.is_some(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err, params.expected_error.unwrap()); + } + } +} diff --git a/contracts/secret-entry-point/tests/test_execute_post_swap_action.rs b/contracts/secret-entry-point/tests/test_execute_post_swap_action.rs new file mode 100644 index 00000000..b71a93ec --- /dev/null +++ b/contracts/secret-entry-point/tests/test_execute_post_swap_action.rs @@ -0,0 +1,723 @@ +/* +use cosmwasm_std::{ + testing::{mock_dependencies_with_balances, mock_env, mock_info}, + to_binary, Addr, BankMsg, Coin, ContractResult, QuerierResult, + ReplyOn::Never, + SubMsg, SystemResult, Timestamp, Uint128, WasmMsg, WasmQuery, +}; +use cw20::{BalanceResponse, Cw20Coin, Cw20ExecuteMsg}; +use secret_skip::{ + asset::Asset, + entry_point::{Action, ExecuteMsg}, + ibc::{ExecuteMsg as IbcTransferExecuteMsg, IbcFee, IbcInfo}, +}; +use skip_go_secret_entry_point::{ + error::ContractError, + state::{BLOCKED_CONTRACT_ADDRESSES, IBC_TRANSFER_CONTRACT_ADDRESS, PRE_SWAP_OUT_ASSET_AMOUNT}, +}; +use test_case::test_case; +*/ + +/* +Test Cases: + +Expect Response + // General + - Native Asset Transfer + - Cw20 Asset Transfer + - Ibc Transfer + - Native Asset Contract Call + - Cw20 Asset Contract Call + + // With IBC Fees + - Ibc Transfer w/ IBC Fees of different denom than min coin + - Ibc Transfer w/ IBC Fees of same denom as min coin + + // Exact Out + - Native Asset Transfer With Exact Out Set To True + - Ibc Transfer With Exact Out Set To True + - Ibc Transfer w/ IBC Fees of different denom than min coin With Exact Out Set To True + - Ibc Transfer w/ IBC Fees of same denom as min coin With Exact Out Set To True + - Contract Call With Exact Out Set To True + + // Invariants + - Pre Swap Out Asset Contract Balance Preserved + +Expect Error + - Transfer Timeout + - Received Less Native Asset From Swap Than Min Asset + - Received Less Cw20 Asset From Swap Than Min Asset + - Unauthorized Caller + - Contract Call Address Blocked + - Cw20 Out Asset With IBC Transfer + */ + +/* +// Define test parameters +struct Params { + caller: String, + min_asset: Asset, + post_swap_action: Action, + exact_out: bool, + pre_swap_out_asset_amount: Uint128, + expected_messages: Vec, + expected_error: Option, +} + +// Test execute_post_swap_action +#[test_case( + Params { + caller: "entry_point".to_string(), + min_asset: Asset::Cw20(Cw20Coin{ + address: "secret123".to_string(), + amount: 1_000_000u128.into(), + }), + post_swap_action: Action::Transfer { + to_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5".to_string(), + }, + exact_out: true, + pre_swap_out_asset_amount: Uint128::new(0), + expected_messages: vec![SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5".to_string(), + amount: vec![Coin::new(100_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Native Asset Transfer With Exact Out Set To True")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_asset: Asset::Native(Coin::new(1_000_000, "os")), + post_swap_action: Action::Transfer { + to_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5".to_string(), + }, + exact_out: false, + pre_swap_out_asset_amount: Uint128::new(0), + expected_messages: vec![SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5".to_string(), + amount: vec![Coin::new(1_000_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Native Asset Transfer")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_asset: Asset::Cw20(Cw20Coin{ + address: "secret123".to_string(), + amount: 1_000_000u128.into(), + }), + post_swap_action: Action::Transfer { + to_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5".to_string(), + }, + exact_out: false, + pre_swap_out_asset_amount: Uint128::new(0), + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "secret123".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5".to_string(), + amount: 1_000_000u128.into(), + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Cw20 Asset Transfer")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_asset: Asset::Native(Coin::new(1_000_000, "os")), + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: None, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: None, + }, + exact_out: false, + pre_swap_out_asset_amount: 0u128.into(), + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "ibc_transfer_adapter".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&IbcTransferExecuteMsg::IbcTransfer { + info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: None, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + coin: Coin::new(1_000_000, "os"), + timeout_timestamp: 101, + }) + .unwrap(), + funds: vec![Coin::new(1_000_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Ibc Transfer")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_asset: Asset::Native(Coin::new(100_000, "os")), + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: None, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: None, + }, + exact_out: true, + pre_swap_out_asset_amount: 0u128.into(), + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "ibc_transfer_adapter".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&IbcTransferExecuteMsg::IbcTransfer { + info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: None, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + coin: Coin::new(100_000, "os"), + timeout_timestamp: 101, + }) + .unwrap(), + funds: vec![Coin::new(100_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Ibc Transfer With Exact Out Set To True")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_asset: Asset::Native(Coin::new(100_000, "os")), + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "un")], + timeout_fee: vec![Coin::new(100_000, "un")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: None, + }, + exact_out: true, + pre_swap_out_asset_amount: Uint128::new(0), + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "ibc_transfer_adapter".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&IbcTransferExecuteMsg::IbcTransfer { + info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "un")], + timeout_fee: vec![Coin::new(100_000, "un")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + coin: Coin::new(100_000, "os"), + timeout_timestamp: 101, + }) + .unwrap(), + funds: vec![ + Coin::new(100_000, "os"), + ], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Ibc Transfer w/ IBC Fees of different denom than min coin With Exact Out Set To True")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_asset: Asset::Native(Coin::new(100_000, "un")), + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "un")], + timeout_fee: vec![Coin::new(100_000, "un")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: None, + }, + exact_out: true, + pre_swap_out_asset_amount: Uint128::new(0), + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "ibc_transfer_adapter".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&IbcTransferExecuteMsg::IbcTransfer { + info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "un")], + timeout_fee: vec![Coin::new(100_000, "un")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + coin: Coin::new(100_000, "un"), + timeout_timestamp: 101, + }) + .unwrap(), + funds: vec![Coin::new(100_000, "un")], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Ibc Transfer w/ IBC Fees of same denom as min coin With Exact Out Set To True")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_asset: Asset::Native(Coin::new(1_000_000, "os")), + post_swap_action: Action::ContractCall { + contract_address: "contract_call".to_string(), + msg: to_binary(&"contract_call_msg").unwrap(), + }, + exact_out: false, + pre_swap_out_asset_amount: Uint128::new(0), + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "contract_call".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&"contract_call_msg").unwrap(), + funds: vec![Coin::new(1_000_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Native Asset Contract Call")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_asset: Asset::Cw20(Cw20Coin{ + address: "secret123".to_string(), + amount: 1_000_000u128.into(), + }), + post_swap_action: Action::ContractCall { + contract_address: "contract_call".to_string(), + msg: to_binary(&"contract_call_msg").unwrap(), + }, + exact_out: false, + pre_swap_out_asset_amount: Uint128::new(0), + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "secret123".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&Cw20ExecuteMsg::Send { + contract: "contract_call".to_string(), + amount: 1_000_000u128.into(), + msg: to_binary(&"contract_call_msg").unwrap(), + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Cw20 Asset Contract Call")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_asset: Asset::Native(Coin::new(100_000, "os")), + post_swap_action: Action::ContractCall { + contract_address: "contract_call".to_string(), + msg: to_binary(&"contract_call_msg").unwrap(), + }, + exact_out: true, + pre_swap_out_asset_amount: Uint128::new(0), + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "contract_call".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&"contract_call_msg").unwrap(), + funds: vec![Coin::new(100_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Contract Call With Exact Out Set To True")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_asset: Asset::Native(Coin::new(1_000_000, "os")), + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "un")], + timeout_fee: vec![Coin::new(100_000, "un")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: None, + }, + exact_out: false, + pre_swap_out_asset_amount: Uint128::new(0), + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "ibc_transfer_adapter".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&IbcTransferExecuteMsg::IbcTransfer { + info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "un")], + timeout_fee: vec![Coin::new(100_000, "un")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + coin: Coin::new(1_000_000, "os"), + timeout_timestamp: 101, + }) + .unwrap(), + funds: vec![ + Coin::new(1_000_000, "os"), + ], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Ibc Transfer w/ IBC Fees of different denom than min coin")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_asset: Asset::Native(Coin::new(800_000, "un")), + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "un")], + timeout_fee: vec![Coin::new(100_000, "un")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: None, + }, + exact_out: false, + pre_swap_out_asset_amount: Uint128::new(0), + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "ibc_transfer_adapter".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&IbcTransferExecuteMsg::IbcTransfer { + info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "un")], + timeout_fee: vec![Coin::new(100_000, "un")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + coin: Coin::new(1_000_000, "un"), + timeout_timestamp: 101, + }) + .unwrap(), + funds: vec![Coin::new(1_000_000, "un")], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Ibc Transfer w/ IBC Fees of same denom as min coin")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_asset: Asset::Native(Coin::new(700_000, "os")), + post_swap_action: Action::Transfer { + to_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5".to_string(), + }, + exact_out: false, + pre_swap_out_asset_amount: Uint128::new(200_000), + expected_messages: vec![SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5".to_string(), + amount: vec![Coin::new(800_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Pre Swap Out Asset Contract Balance Preserved")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_asset: Asset::Cw20(Cw20Coin{ + address: "secret123".to_string(), + amount: 1_000_000u128.into(), + }), + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: None, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: None, + }, + exact_out: false, + pre_swap_out_asset_amount: Uint128::new(0), + expected_messages: vec![], + expected_error: Some(ContractError::NonNativeIbcTransfer), + }; + "Cw20 Out Asset With IBC Transfer")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_asset: Asset::Native(Coin::new(1_100_000, "un")), + post_swap_action: Action::Transfer { + to_address: "swapper".to_string(), + }, + exact_out: false, + pre_swap_out_asset_amount: Uint128::new(0), + expected_messages: vec![], + expected_error: Some(ContractError::ReceivedLessAssetFromSwapsThanMinAsset), + }; + "Received Less Native Asset From Swap Than Min Asset - Expect Error")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_asset: Asset::Cw20(Cw20Coin{ + address: "secret123".to_string(), + amount: 2_100_000u128.into(), + }), + post_swap_action: Action::Transfer { + to_address: "swapper".to_string(), + }, + exact_out: false, + pre_swap_out_asset_amount: Uint128::new(0), + expected_messages: vec![], + expected_error: Some(ContractError::ReceivedLessAssetFromSwapsThanMinAsset), + }; + "Received Less Cw20 Asset From Swap Than Min Asset - Expect Error")] +#[test_case( + Params { + caller: "unauthorized".to_string(), + min_asset: Asset::Native(Coin::new(1_100_000, "un")), + post_swap_action: Action::Transfer { + to_address: "swapper".to_string(), + }, + exact_out: false, + pre_swap_out_asset_amount: Uint128::new(0), + expected_messages: vec![], + expected_error: Some(ContractError::Unauthorized), + }; + "Unauthorized Caller - Expect Error")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_asset: Asset::Native(Coin::new(900_000, "un")), + post_swap_action: Action::ContractCall { + contract_address: "entry_point".to_string(), + msg: to_binary(&"contract_call_msg").unwrap(), + }, + exact_out: false, + pre_swap_out_asset_amount: Uint128::new(0), + expected_messages: vec![], + expected_error: Some(ContractError::ContractCallAddressBlocked), + }; + "Contract Call Address Blocked - Expect Error")] +fn test_execute_post_swap_action(params: Params) { + // Create mock dependencies + let mut deps = mock_dependencies_with_balances(&[( + "entry_point", + &[Coin::new(1_000_000, "os"), Coin::new(1_000_000, "un")], + )]); + + // Create mock wasm handler to handle the swap adapter contract query + let wasm_handler = |query: &WasmQuery| -> QuerierResult { + match query { + WasmQuery::Smart { .. } => SystemResult::Ok(ContractResult::Ok( + to_binary(&BalanceResponse { + balance: Uint128::from(1_000_000u128), + }) + .unwrap(), + )), + _ => panic!("Unsupported query: {:?}", query), + } + }; + + // Update querier with mock wasm handler + deps.querier.update_wasm(wasm_handler); + + // Create mock env with parameters that make testing easier + let mut env = mock_env(); + env.contract.address = Addr::unchecked("entry_point"); + env.block.time = Timestamp::from_nanos(100); + + // Create mock info with entry point contract address + let info = mock_info(¶ms.caller, &[]); + + // Store the ibc transfer adapter contract address + let ibc_transfer_adapter = Addr::unchecked("ibc_transfer_adapter"); + IBC_TRANSFER_CONTRACT_ADDRESS + .save(deps.as_mut().storage, &ibc_transfer_adapter) + .unwrap(); + + // Store the entry point contract address in the blocked contract addresses map + BLOCKED_CONTRACT_ADDRESSES + .save(deps.as_mut().storage, &Addr::unchecked("entry_point"), &()) + .unwrap(); + + // Store the pre swap out asset amount + PRE_SWAP_OUT_ASSET_AMOUNT + .save(deps.as_mut().storage, ¶ms.pre_swap_out_asset_amount) + .unwrap(); + + // Call execute_post_swap_action with the given test parameters + let res = skip_go_entry_point::contract::execute( + deps.as_mut(), + env, + info, + ExecuteMsg::PostSwapAction { + min_asset: params.min_asset, + timeout_timestamp: 101, + post_swap_action: params.post_swap_action, + exact_out: params.exact_out, + }, + ); + + match res { + Ok(res) => { + // Assert the test did not expect an error + assert!( + params.expected_error.is_none(), + "expected test to error with {:?}, but it succeeded", + params.expected_error + ); + + // Assert the number of messages in the response is correct + assert_eq!( + res.messages.len(), + params.expected_messages.len(), + "expected {:?} messages, but got {:?}", + params.expected_messages.len(), + res.messages.len() + ); + + // Assert the messages in the response are correct + assert_eq!(res.messages, params.expected_messages,); + } + Err(err) => { + // Assert the test expected an error + assert!( + params.expected_error.is_some(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err, params.expected_error.unwrap()); + } + } +} +*/ diff --git a/contracts/secret-entry-point/tests/test_execute_receive.rs b/contracts/secret-entry-point/tests/test_execute_receive.rs new file mode 100644 index 00000000..960e6628 --- /dev/null +++ b/contracts/secret-entry-point/tests/test_execute_receive.rs @@ -0,0 +1,382 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{ + testing::{mock_dependencies_with_balances, mock_env, mock_info}, + to_binary, Addr, Coin, ContractInfo, ContractResult, QuerierResult, + ReplyOn::{Always, Never}, + StdError, SubMsg, SystemError, SystemResult, Timestamp, Uint128, WasmMsg, WasmQuery, +}; +use secret_skip::{ + asset::Asset, + cw20::Cw20Coin, + snip20::Snip20ReceiveMsg, + swap::{Swap, SwapExactAssetIn, SwapOperation}, +}; +use secret_toolkit::snip20::{AuthenticatedQueryResponse, Balance}; +use skip_go_secret_entry_point::{ + error::ContractError, + msg::{Action, Affiliate, ExecuteMsg, Snip20HookMsg}, + reply::RECOVER_REPLY_ID, + state::{IBC_TRANSFER_CONTRACT, REGISTERED_TOKENS, SWAP_VENUE_MAP, VIEWING_KEY}, +}; +use test_case::test_case; + +#[cw_serde] +pub enum Snip20Response { + Balance { amount: Uint128 }, +} + +/* +Test Cases: + +Expect Response + - Valid Swap And Action Msg + - Valid Swap And Action With Recover Msg + */ + +// Define test parameters +struct Params { + info_funds: Vec, + sent_asset: Asset, + user_swap: Swap, + min_asset: Asset, + timeout_timestamp: u64, + post_swap_action: Action, + affiliates: Vec, + recovery_addr: Option, + expected_messages: Vec, + expected_error: Option, +} + +// Test execute_receive +#[test_case( + Params { + info_funds: vec![], + sent_asset: Asset::Cw20(Cw20Coin { + address: "secret123".to_string(), + amount: 1_000_000u128.into(), + }), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "secret123".to_string(), + denom_out: "secret456".to_string(), + interface: None, + } + ], + } + ), + min_asset: Asset::Cw20(Cw20Coin { + address: "secret456".to_string(), + amount: 1000u128.into(), + }), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + recovery_addr: None, + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&ExecuteMsg::UserSwap { + swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "secret123".to_string(), + denom_out: "secret456".to_string(), + interface: None, + } + ], + } + ), + remaining_asset: Asset::Cw20(Cw20Coin { + address: "secret123".to_string(), + amount: 1_000_000u128.into(), + }), + min_asset: Asset::Cw20(Cw20Coin { + address: "secret456".to_string(), + amount: 1000u128.into(), + }), + affiliates: vec![], + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&ExecuteMsg::PostSwapAction { + min_asset: Asset::Cw20(Cw20Coin { + address: "secret456".to_string(), + amount: 1000u128.into(), + }), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + exact_out: false, + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Valid Swap And Action Msg")] +#[test_case( + Params { + info_funds: vec![], + sent_asset: Asset::Cw20(Cw20Coin{address: "secret123".to_string(), amount: 1_000_000u128.into()}), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "secret123".to_string(), + denom_out: "osmo".to_string(), + interface: None, + } + ], + } + ), + min_asset: Asset::Cw20(Cw20Coin { + address: "secret456".to_string(), + amount: 1000u128.into(), + }), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + recovery_addr: Some(Addr::unchecked("recovery_addr")), + expected_messages: vec![ + SubMsg { + id: RECOVER_REPLY_ID, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&ExecuteMsg::SwapAndAction { + sent_asset: Some(Asset::Cw20(Cw20Coin{address: "secret123".to_string(), amount: 1_000_000u128.into()})), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "secret123".to_string(), + denom_out: "osmo".to_string(), + interface: None, + } + ], + } + ), + min_asset: Asset::Cw20(Cw20Coin { + address: "secret456".to_string(), + amount: 1000u128.into(), + }), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![] }) + .unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Always, + }, + ], + expected_error: None, + }; + "Valid Swap And Action With Recover Msg")] +fn test_execute_receive(params: Params) { + // Create mock dependencies + let mut deps = mock_dependencies_with_balances(&[("entry_point", &[])]); + + // Create mock wasm handler to handle the swap adapter contract query + let wasm_handler = |query: &WasmQuery| -> QuerierResult { + match query { + WasmQuery::Smart { contract_addr, .. } => { + if contract_addr == "swap_venue_adapter" { + SystemResult::Ok(ContractResult::Ok( + to_binary(&Asset::Native(Coin::new(200_000, "osmo"))).unwrap(), + )) + } else if vec!["secret123", "secret456"].contains(&contract_addr.as_str()) { + SystemResult::Ok(ContractResult::Ok( + to_binary(&Snip20Response::Balance { + amount: 1_000_000u128.into(), + }) + .unwrap(), + )) + } else { + SystemResult::Err(SystemError::UnsupportedRequest { + kind: format!("query {}", contract_addr), + }) + } + } + _ => panic!("Unsupported query: {:?}", query), + } + }; + + // Update querier with mock wasm handler + deps.querier.update_wasm(wasm_handler); + + // Create mock env with parameters that make testing easier + let mut env = mock_env(); + env.contract.address = Addr::unchecked("entry_point"); + env.contract.code_hash = "code_hash".to_string(); + env.block.time = Timestamp::from_nanos(100); + + // Convert info funds vector into a slice of Coin objects + let info_funds: &[Coin] = ¶ms.info_funds; + + // Create mock info with entry point contract address + let info = mock_info("secret123", info_funds); + + // Store the swap venue adapter contract address + let swap_venue_adapter = Addr::unchecked("swap_venue_adapter"); + SWAP_VENUE_MAP + .save( + deps.as_mut().storage, + "swap_venue_name", + &ContractInfo { + address: swap_venue_adapter, + code_hash: "code_hash".to_string(), + }, + ) + .unwrap(); + + // Store the ibc transfer adapter contract address + let ibc_transfer_adapter = ContractInfo { + address: Addr::unchecked("ibc_transfer_adapter"), + code_hash: "code_hash".to_string(), + }; + IBC_TRANSFER_CONTRACT + .save(deps.as_mut().storage, &ibc_transfer_adapter) + .unwrap(); + + REGISTERED_TOKENS + .save( + deps.as_mut().storage, + Addr::unchecked("secret123"), + &ContractInfo { + address: Addr::unchecked("secret123"), + code_hash: "code_hash".to_string(), + }, + ) + .unwrap(); + REGISTERED_TOKENS + .save( + deps.as_mut().storage, + Addr::unchecked(params.min_asset.clone().denom()), + &ContractInfo { + address: Addr::unchecked(params.min_asset.clone().denom()), + code_hash: "code_hash".to_string(), + }, + ) + .unwrap(); + + VIEWING_KEY + .save(deps.as_mut().storage, &"viewing_key".to_string()) + .unwrap(); + + // Call execute_receive with the given test case params + let res = match params.recovery_addr { + Some(recovery_addr) => skip_go_secret_entry_point::contract::execute( + deps.as_mut(), + env, + info, + ExecuteMsg::Receive(Snip20ReceiveMsg { + sender: Addr::unchecked("swapper".to_string()), + amount: params.sent_asset.amount(), + from: Addr::unchecked("swapper".to_string()), + memo: None, + msg: Some( + to_binary(&Snip20HookMsg::SwapAndActionWithRecover { + user_swap: params.user_swap, + min_asset: params.min_asset.clone(), + timeout_timestamp: params.timeout_timestamp, + post_swap_action: params.post_swap_action, + affiliates: params.affiliates, + recovery_addr, + }) + .unwrap(), + ), + }), + ), + None => skip_go_secret_entry_point::contract::execute( + deps.as_mut(), + env, + info, + ExecuteMsg::Receive(Snip20ReceiveMsg { + sender: Addr::unchecked("swapper".to_string()), + amount: params.sent_asset.amount(), + from: Addr::unchecked("swapper".to_string()), + memo: None, + msg: Some( + to_binary(&Snip20HookMsg::SwapAndAction { + user_swap: params.user_swap, + min_asset: params.min_asset, + timeout_timestamp: params.timeout_timestamp, + post_swap_action: params.post_swap_action, + affiliates: params.affiliates, + }) + .unwrap(), + ), + }), + ), + }; + + match res { + Ok(res) => { + // Assert the test did not expect an error + assert!( + params.expected_error.is_none(), + "expected test to error with {:?}, but it succeeded", + params.expected_error + ); + + // Assert the number of messages in the response is correct + assert_eq!( + res.messages.len(), + params.expected_messages.len(), + "expected {:?} messages, but got {:?}", + params.expected_messages.len(), + res.messages.len() + ); + + // Assert the messages in the response are correct + assert_eq!(res.messages, params.expected_messages,); + } + Err(err) => { + // Assert the test expected an error + assert!( + params.expected_error.is_some(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err, params.expected_error.unwrap()); + } + } +} diff --git a/contracts/secret-entry-point/tests/test_execute_swap_and_action.rs b/contracts/secret-entry-point/tests/test_execute_swap_and_action.rs new file mode 100644 index 00000000..12a56de0 --- /dev/null +++ b/contracts/secret-entry-point/tests/test_execute_swap_and_action.rs @@ -0,0 +1,1969 @@ +/* +use cosmwasm_std::{ + testing::{mock_dependencies_with_balances, mock_env, mock_info}, + to_json_binary, Addr, BankMsg, Coin, ContractResult, OverflowError, OverflowOperation, + QuerierResult, + ReplyOn::Never, + SubMsg, SystemResult, Timestamp, Uint128, WasmMsg, WasmQuery, +}; +use cw20::{BalanceResponse, Cw20Coin, Cw20ExecuteMsg}; +use cw_utils::PaymentError::{MultipleDenoms, NoFunds, NonPayable}; +use skip::{ + asset::Asset, + entry_point::{Action, Affiliate, ExecuteMsg}, + error::SkipError::{ + IbcFeesNotOneCoin, InvalidCw20Coin, Overflow, Payment, SwapOperationsAssetInDenomMismatch, + SwapOperationsAssetOutDenomMismatch, SwapOperationsEmpty, + }, + ibc::{IbcFee, IbcInfo}, + swap::{ + ExecuteMsg as SwapExecuteMsg, Route, SmartSwapExactAssetIn, Swap, SwapExactAssetIn, + SwapExactAssetOut, SwapOperation, + }, +}; +use skip_go_entry_point::{ + error::ContractError, + state::{IBC_TRANSFER_CONTRACT_ADDRESS, PRE_SWAP_OUT_ASSET_AMOUNT, SWAP_VENUE_MAP}, +}; +use test_case::test_case; +*/ + +/* +Test Cases: + +Expect Response + Native Asset + - User Swap Exact Coin In With Transfer + - User Swap Exact Coin Out With Transfer + - User Swap Exact Coin In With IBC Transfer With IBC Fees + - User Swap Exact Coin In With IBC Transfer Without IBC Fees + - Fee Swap And User Swap Exact Coin In With IBC Fees + - Sent Asset Not Given With Valid One Coin + + CW20 Asset + - User Swap Exact Cw20 Asset In With Transfer + - Fee Swap And User Swap Exact Cw20 Asset In With IBC Fees + - Cw20 Min Asset + +Expect Error + // Fee Swap + - Fee Swap Coin In Amount More Than Remaining Coin Received Amount + - Fee Swap Coin In Denom Is Not The Same As Remaining Coin Received Denom + - Fee Swap First Swap Operation Denom In Is Not The Same As Remaining Coin Received Denom + - Fee Swap Last Swap Operation Denom Out Is Not The Same As IBC Fee Coin Denom + - Fee Swap With IBC Transfer But Without IBC Fees + + // User Swap + - User Swap With IBC Transfer With IBC Fees But IBC Fee Coin Denom Is Not The Same As Remaining Coin Received Denom + + // Invalid Assets Sent To Contract + - No Coins Sent To Contract + - More Than One Coin Sent To Contract + - Not Enough Cw20 Tokens Sent To Contract + - Cw20 Token Specified As Sent Asset With Native Coin Sent To Contract + + // Empty Swap Operations + - Empty Fee Swap Operations + + // Timeout + - Current Block Time Greater Than Timeout Timestamp + + // IBC Transfer + - IBC Transfer With IBC Fees But More Than One IBC Fee Denom Specified + - IBC Transfer With IBC Fees But No IBC Fee Coins Specified + - IBC Transfer With IBC Fee Coin Amount Zero + + // Sent Asset + - Sent Asset Not Given with Invalid One Coin + */ + +/* +// Define test parameters +struct Params { + info_funds: Vec, + sent_asset: Option, + user_swap: Swap, + min_asset: Asset, + timeout_timestamp: u64, + post_swap_action: Action, + affiliates: Vec, + expected_messages: Vec, + expected_error: Option, +} + +// Test execute_swap_and_action +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "untrn"), + ], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "untrn"))), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + } + ], + } + ), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::UserSwap { + swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + } + ], + } + ), + remaining_asset: Asset::Native(Coin::new(1_000_000, "untrn")), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + affiliates: vec![], + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::PostSwapAction { + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + exact_out: false, + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "User Swap Exact Coin In With Transfer")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "untrn"), + ], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "untrn"))), + user_swap: Swap::SwapExactAssetOut ( + SwapExactAssetOut{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + } + ], + refund_address: Some("refund_address".to_string()), + } + ), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::UserSwap { + swap: Swap::SwapExactAssetOut ( + SwapExactAssetOut{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + } + ], + refund_address: Some("refund_address".to_string()), + } + ), + remaining_asset: Asset::Native(Coin::new(1_000_000, "untrn")), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + affiliates: vec![], + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::PostSwapAction { + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + exact_out: true, + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "User Swap Exact Coin Out With Transfer")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "untrn"), + ], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "untrn"))), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + } + ], + }, + ), + min_asset: Asset::Native(Coin::new(800_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: None, + }, + affiliates: vec![], + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "ibc_transfer_adapter".to_string(), + amount: vec![Coin::new(200_000, "untrn")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::UserSwap { + swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + } + ], + } + ), + remaining_asset: Asset::Native(Coin::new(800_000, "untrn")), + min_asset: Asset::Native(Coin::new(800_000, "osmo")), + affiliates: vec![], + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::PostSwapAction { + min_asset: Asset::Native(Coin::new(800_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: None, + }, + exact_out: false, + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "User Swap Exact Coin In With IBC Transfer With IBC Fees")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "untrn"), + ], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "untrn"))), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + } + ], + }, + ), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: None, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: None, + }, + affiliates: vec![], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::UserSwap { + swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + } + ], + } + ), + remaining_asset: Asset::Native(Coin::new(1_000_000, "untrn")), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + affiliates: vec![], + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::PostSwapAction { + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: None, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: None, + }, + exact_out: false, + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "User Swap Exact Coin In With IBC Transfer Without IBC Fees")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "osmo"), + ], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "osmo"))), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "uatom".to_string(), + interface: None, + } + ], + }, + ), + min_asset: Asset::Native(Coin::new(100_000, "uatom")), + timeout_timestamp: 101, + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: Some( + SwapExactAssetOut { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "osmo".to_string(), + denom_out: "untrn".to_string(), + interface: None, + } + ], + refund_address: None, + } + ), + }, + affiliates: vec![], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_venue_adapter".to_string(), + msg: to_json_binary(&SwapExecuteMsg::Swap { + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "osmo".to_string(), + denom_out: "untrn".to_string(), + interface: None, + } + ], + }).unwrap(), + funds: vec![Coin::new(200_000, "osmo")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "ibc_transfer_adapter".to_string(), + amount: vec![Coin::new(200_000, "untrn")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::UserSwap { + swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "uatom".to_string(), + interface: None, + } + ], + } + ), + remaining_asset: Asset::Native(Coin::new(800_000, "osmo")), + min_asset: Asset::Native(Coin::new(100_000, "uatom")), + affiliates: vec![], + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::PostSwapAction { + min_asset: Asset::Native(Coin::new(100_000, "uatom")), + timeout_timestamp: 101, + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: Some( + SwapExactAssetOut { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "osmo".to_string(), + denom_out: "untrn".to_string(), + interface: None, + } + ], + refund_address: None, + } + ), + }, + exact_out: false, + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Fee Swap And User Swap Exact Coin In With IBC Fees")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "untrn"), + ], + sent_asset: None, + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + } + ], + } + ), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::UserSwap { + swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + } + ], + } + ), + remaining_asset: Asset::Native(Coin::new(1_000_000, "untrn")), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + affiliates: vec![], + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::PostSwapAction { + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + exact_out: false, + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Sent Asset Not Given With Valid One Coin")] +#[test_case( + Params { + info_funds: vec![], + sent_asset: Some(Asset::Cw20(Cw20Coin { + address: "neutron123".to_string(), + amount: Uint128::from(1_000_000u128), + })), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "neutron123".to_string(), + denom_out: "osmo".to_string(), + interface: None, + } + ], + } + ), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::UserSwap { + swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "neutron123".to_string(), + denom_out: "osmo".to_string(), + interface: None, + } + ], + } + ), + remaining_asset: Asset::Cw20(Cw20Coin { + address: "neutron123".to_string(), + amount: Uint128::from(1_000_000u128), + }), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + affiliates: vec![], + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::PostSwapAction { + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + exact_out: false, + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "User Swap Exact Cw20 Asset In With Transfer")] +#[test_case( + Params { + info_funds: vec![], + sent_asset: Some(Asset::Cw20(Cw20Coin { + address: "neutron123".to_string(), + amount: Uint128::from(1_000_000u128), + })), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "neutron123".to_string(), + denom_out: "uatom".to_string(), + interface: None, + } + ], + }, + ), + min_asset: Asset::Native(Coin::new(100_000, "uatom")), + timeout_timestamp: 101, + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: Some( + SwapExactAssetOut { + swap_venue_name: "swap_venue_name_2".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "neutron123".to_string(), + denom_out: "untrn".to_string(), + interface: None, + } + ], + refund_address: None, + } + ), + }, + affiliates: vec![], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "neutron123".to_string(), + msg: to_json_binary(&Cw20ExecuteMsg::Send { + contract: "swap_venue_adapter_2".to_string(), + amount: Uint128::from(200_000u128), + msg: to_json_binary(&SwapExecuteMsg::Swap { + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "neutron123".to_string(), + denom_out: "untrn".to_string(), + interface: None, + } + ], + }).unwrap(), + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "ibc_transfer_adapter".to_string(), + amount: vec![Coin::new(200_000, "untrn")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::UserSwap { + swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "neutron123".to_string(), + denom_out: "uatom".to_string(), + interface: None, + } + ], + } + ), + remaining_asset: Asset::Cw20(Cw20Coin { + address: "neutron123".to_string(), + amount: Uint128::from(800_000u128), + }), + min_asset: Asset::Native(Coin::new(100_000, "uatom")), + affiliates: vec![], + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::PostSwapAction { + min_asset: Asset::Native(Coin::new(100_000, "uatom")), + timeout_timestamp: 101, + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: Some( + SwapExactAssetOut { + swap_venue_name: "swap_venue_name_2".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "neutron123".to_string(), + denom_out: "untrn".to_string(), + interface: None, + } + ], + refund_address: None, + } + ), + }, + exact_out: false, + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Fee Swap And User Swap Exact Cw20 Asset In With IBC Fees")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "untrn"), + ], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "untrn"))), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + } + ], + } + ), + min_asset: Asset::Cw20(Cw20Coin { + address: "neutron123".to_string(), + amount: Uint128::from(1_000_000u128), + }), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::UserSwap { + swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + } + ], + } + ), + remaining_asset: Asset::Native(Coin::new(1_000_000, "untrn")), + min_asset: Asset::Cw20(Cw20Coin { + address: "neutron123".to_string(), + amount: Uint128::from(1_000_000u128), + }), + affiliates: vec![], + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::PostSwapAction { + min_asset: Asset::Cw20(Cw20Coin { + address: "neutron123".to_string(), + amount: Uint128::from(1_000_000u128), + }), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + exact_out: false, + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Cw20 Min Asset")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(100_000, "osmo"), + ], + sent_asset: Some(Asset::Native(Coin::new(100_000, "osmo"))), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "uatom".to_string(), + interface: None, + } + ], + }, + ), + min_asset: Asset::Native(Coin::new(100_000, "uatom")), + timeout_timestamp: 101, + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: Some( + SwapExactAssetOut { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "osmo".to_string(), + denom_out: "untrn".to_string(), + interface: None, + } + ], + refund_address: None, + } + ), + }, + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Skip(Overflow(OverflowError { + operation: OverflowOperation::Sub, + operand1: "100000".to_string(), + operand2: "200000".to_string(), + }))), + }; + "Fee Swap Coin In Amount More Than Remaining Coin Received Amount- Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "uatom"), + ], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "uatom"))), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "uatom".to_string(), + interface: None, + } + ], + }, + ), + min_asset: Asset::Native(Coin::new(100_000, "uatom")), + timeout_timestamp: 101, + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: Some( + SwapExactAssetOut { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "uatom".to_string(), + denom_out: "untrn".to_string(), + interface: None, + } + ], + refund_address: None, + } + ), + }, + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::FeeSwapAssetInDenomMismatch), + }; + "Fee Swap Coin In Denom In Not The Same As Remaining Coin Received Denom - Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "osmo"), + ], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "osmo"))), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "uatom".to_string(), + interface: None, + } + ], + }, + ), + min_asset: Asset::Native(Coin::new(100_000, "uatom")), + timeout_timestamp: 101, + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: Some( + SwapExactAssetOut { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "uatom".to_string(), + denom_out: "untrn".to_string(), + interface: None, + } + ], + refund_address: None, + } + ), + }, + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Skip(SwapOperationsAssetInDenomMismatch)), + }; + "Fee Swap First Swap Operation Denom In Is Not The Same As Remaining Coin Received Denom - Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "osmo"), + ], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "osmo"))), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "uatom".to_string(), + interface: None, + } + ], + }, + ), + min_asset: Asset::Native(Coin::new(100_000, "uatom")), + timeout_timestamp: 101, + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: Some( + SwapExactAssetOut { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "osmo".to_string(), + denom_out: "osmo".to_string(), + interface: None, + } + ], + refund_address: None, + } + ), + }, + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Skip(SwapOperationsAssetOutDenomMismatch)), + }; + "Fee Swap Last Swap Operation Denom Out Is Not The Same As IBC Fee Coin Denom- Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "untrn"), + ], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "untrn"))), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + } + ], + }, + ), + min_asset: Asset::Native(Coin::new(800_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "uatom")], + timeout_fee: vec![Coin::new(100_000, "uatom")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: None, + }, + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::IBCFeeDenomDiffersFromAssetReceived), + }; + "User Swap With IBC Transfer With IBC Fees But IBC Fee Coin Denom Is Not The Same As Remaining Coin Received Denom - Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "osmo"), + ], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "osmo"))), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "atom".to_string(), + interface: None, + } + ], + }, + ), + min_asset: Asset::Native(Coin::new(100_000, "atom")), + timeout_timestamp: 101, + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: None, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: Some( + SwapExactAssetOut { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "osmo".to_string(), + denom_out: "untrn".to_string(), + interface: None, + } + ], + refund_address: None, + } + ), + }, + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::FeeSwapWithoutIbcFees), + }; + "Fee Swap With IBC Trnasfer But Without IBC Fees - Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "osmo"), + ], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "osmo"))), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "atom".to_string(), + interface: None, + } + ], + }, + ), + min_asset: Asset::Native(Coin::new(100_000, "atom")), + timeout_timestamp: 101, + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "uatom")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: Some( + SwapExactAssetOut { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "osmo".to_string(), + denom_out: "untrn".to_string(), + interface: None, + } + ], + refund_address: None, + } + ), + }, + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Skip(IbcFeesNotOneCoin)), + }; + "IBC Transfer With IBC Fees But More Than One IBC Fee Denom Specified - Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "osmo"), + ], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "osmo"))), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "atom".to_string(), + interface: None, + } + ], + }, + ), + min_asset: Asset::Native(Coin::new(100_000, "atom")), + timeout_timestamp: 101, + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![], + timeout_fee: vec![], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: Some( + SwapExactAssetOut { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "osmo".to_string(), + denom_out: "untrn".to_string(), + interface: None, + } + ], + refund_address: None, + } + ), + }, + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Skip(IbcFeesNotOneCoin)), + }; + "IBC Transfer With IBC Fees But No IBC Fee Coins Specified - Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "osmo"), + ], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "osmo"))), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "atom".to_string(), + interface: None, + } + ], + }, + ), + min_asset: Asset::Native(Coin::new(100_000, "atom")), + timeout_timestamp: 101, + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![Coin::new(0, "uatom")], + ack_fee: vec![], + timeout_fee: vec![], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: Some( + SwapExactAssetOut { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "osmo".to_string(), + denom_out: "untrn".to_string(), + interface: None, + } + ], + refund_address: None, + } + ), + }, + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Skip(IbcFeesNotOneCoin)), + }; + "IBC Transfer With IBC Fee Coin Amount Zero - Expect Error")] +#[test_case( + Params { + info_funds: vec![], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "osmo"))), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + } + ], + }, + ), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Skip(Payment(NoFunds{}))), + }; + "No Coins Sent to Contract - Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "untrn"), + Coin::new(1_000_000, "osmo"), + ], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "untrn"))), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + } + ], + }, + ), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Skip(Payment(MultipleDenoms{}))), + }; + "More Than One Coin Sent to Contract - Expect Error")] +#[test_case( + Params { + info_funds: vec![], + sent_asset: Some(Asset::Cw20(Cw20Coin { + address: "neutron123".to_string(), + amount: Uint128::from(2_000_000u128), + })), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + } + ], + }, + ), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Skip(InvalidCw20Coin)), + }; + "Not Enough Cw20 Tokens Sent To Contract - Expect Error")] +#[test_case( + Params { + info_funds: vec![Coin::new(1_000_000, "osmo")], + sent_asset: Some(Asset::Cw20(Cw20Coin { + address: "neutron123".to_string(), + amount: Uint128::from(1_000_000u128), + })), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + } + ], + }, + ), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Skip(Payment(NonPayable{}))), + }; + "Cw20 Token Specified As Sent Asset With Native Coin Sent To Contract - Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "osmo"), + ], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "osmo"))), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "uatom".to_string(), + interface: None, + } + ], + }, + ), + min_asset: Asset::Native(Coin::new(100_000, "uatom")), + timeout_timestamp: 101, + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: Some( + SwapExactAssetOut { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![], + refund_address: None, + } + ), + }, + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Skip(SwapOperationsEmpty)), + }; + "Empty Fee Swap Operations - Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "untrn"), + ], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "untrn"))), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![], + }, + ), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 99, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Timeout), + }; + "Current Block Time Greater Than Timeout Timestamp - Expect Error")] +#[test_case( + Params { + info_funds: vec![], + sent_asset: None, + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + } + ], + } + ), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Payment(NoFunds{})), + }; + "Sent Asset Not Given with Invalid One Coin")] +#[test_case( + Params { + info_funds: vec![Coin::new(1_000_000, "untrn")], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "untrn"))), + user_swap: Swap::SmartSwapExactAssetIn(SmartSwapExactAssetIn { + swap_venue_name: "swap_venue_name".to_string(), + routes: vec![ + Route { + offer_asset: Asset::Native(Coin::new(250_000, "untrn")), + operations: vec![SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + }], + }, + Route { + offer_asset: Asset::Native(Coin::new(750_000, "untrn")), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "untrn".to_string(), + denom_out: "neutron123".to_string(), + interface: None, + }, + SwapOperation { + pool: "pool_3".to_string(), + denom_in: "neutron123".to_string(), + denom_out: "osmo".to_string(), + interface: None, + }, + ], + }, + ], + }), + min_asset: Asset::Native(Coin::new(800_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5".to_string(), + }, + fee_swap: None, + }, + affiliates: vec![], + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "ibc_transfer_adapter".to_string(), + amount: vec![Coin::new(200_000, "untrn")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::UserSwap { + swap: Swap::SmartSwapExactAssetIn(SmartSwapExactAssetIn { + swap_venue_name: "swap_venue_name".to_string(), + routes: vec![ + Route { + offer_asset: Asset::Native(Coin::new(250_000, "untrn")), + operations: vec![SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + }], + }, + Route { + offer_asset: Asset::Native(Coin::new(550_000, "untrn")), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "untrn".to_string(), + denom_out: "neutron123".to_string(), + interface: None, + }, + SwapOperation { + pool: "pool_3".to_string(), + denom_in: "neutron123".to_string(), + denom_out: "osmo".to_string(), + interface: None, + }, + ], + }, + ], + }), + remaining_asset: Asset::Native(Coin::new(800_000, "untrn")), + min_asset: Asset::Native(Coin::new(800_000, "osmo")), + affiliates: vec![], + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::PostSwapAction { + min_asset: Asset::Native(Coin::new(800_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: None, + }, + exact_out: false, + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Adjusts SmartSwapExactAssetIn route offer_assets sum to match remaining_asset" +)] +fn test_execute_swap_and_action(params: Params) { + // Create mock dependencies + let mut deps = mock_dependencies_with_balances(&[( + "entry_point", + &[Coin::new(1_000_000, "osmo"), Coin::new(1_000_000, "untrn")], + )]); + + // Create mock wasm handler to handle the swap adapter contract query + let wasm_handler = |query: &WasmQuery| -> QuerierResult { + match query { + WasmQuery::Smart { contract_addr, .. } => { + if contract_addr == "swap_venue_adapter" { + SystemResult::Ok(ContractResult::Ok( + to_json_binary(&Asset::Native(Coin::new(200_000, "osmo"))).unwrap(), + )) + } else if contract_addr == "swap_venue_adapter_2" { + SystemResult::Ok(ContractResult::Ok( + to_json_binary(&Asset::Cw20(Cw20Coin { + address: "neutron123".to_string(), + amount: Uint128::from(200_000u128), + })) + .unwrap(), + )) + } else { + SystemResult::Ok(ContractResult::Ok( + to_json_binary(&BalanceResponse { + balance: Uint128::from(1_000_000u128), + }) + .unwrap(), + )) + } + } + _ => panic!("Unsupported query: {:?}", query), + } + }; + + // Update querier with mock wasm handler + deps.querier.update_wasm(wasm_handler); + + // Create mock env with parameters that make testing easier + let mut env = mock_env(); + env.contract.address = Addr::unchecked("entry_point"); + env.block.time = Timestamp::from_nanos(100); + + // Convert info funds vector into a slice of Coin objects + let info_funds: &[Coin] = ¶ms.info_funds; + + // Create mock info with entry point contract address + let info = mock_info("swapper", info_funds); + + // Store the swap venue adapter contract address + let swap_venue_adapter = Addr::unchecked("swap_venue_adapter"); + let swap_venue_adapter_2 = Addr::unchecked("swap_venue_adapter_2"); + SWAP_VENUE_MAP + .save( + deps.as_mut().storage, + "swap_venue_name", + &swap_venue_adapter, + ) + .unwrap(); + SWAP_VENUE_MAP + .save( + deps.as_mut().storage, + "swap_venue_name_2", + &swap_venue_adapter_2, + ) + .unwrap(); + + // Store the ibc transfer adapter contract address + let ibc_transfer_adapter = Addr::unchecked("ibc_transfer_adapter"); + IBC_TRANSFER_CONTRACT_ADDRESS + .save(deps.as_mut().storage, &ibc_transfer_adapter) + .unwrap(); + + // Call execute_swap_and_action with the given test case params + let res = skip_go_entry_point::contract::execute( + deps.as_mut(), + env, + info, + ExecuteMsg::SwapAndAction { + sent_asset: params.sent_asset, + user_swap: params.user_swap, + min_asset: params.min_asset, + timeout_timestamp: params.timeout_timestamp, + post_swap_action: params.post_swap_action, + affiliates: params.affiliates, + }, + ); + + match res { + Ok(res) => { + // Assert the test did not expect an error + assert!( + params.expected_error.is_none(), + "expected test to error with {:?}, but it succeeded", + params.expected_error + ); + + // Assert the number of messages in the response is correct + assert_eq!( + res.messages.len(), + params.expected_messages.len(), + "expected {:?} messages, but got {:?}", + params.expected_messages.len(), + res.messages.len() + ); + + // Assert the messages in the response are correct + assert_eq!(res.messages, params.expected_messages,); + + // Assert the pre swap out asset amount set is correct + let pre_swap_out_asset_amount = PRE_SWAP_OUT_ASSET_AMOUNT.load(&deps.storage).unwrap(); + assert_eq!(pre_swap_out_asset_amount, Uint128::from(1_000_000u128)); + } + Err(err) => { + // Assert the test expected an error + assert!( + params.expected_error.is_some(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err, params.expected_error.unwrap()); + } + } +} +*/ diff --git a/contracts/secret-entry-point/tests/test_execute_swap_and_action_with_recover.rs b/contracts/secret-entry-point/tests/test_execute_swap_and_action_with_recover.rs new file mode 100644 index 00000000..b33ac046 --- /dev/null +++ b/contracts/secret-entry-point/tests/test_execute_swap_and_action_with_recover.rs @@ -0,0 +1,447 @@ +/* +use cosmwasm_std::{ + testing::{mock_dependencies_with_balances, mock_env, mock_info}, + to_json_binary, Addr, Coin, CosmosMsg, ReplyOn, SubMsg, Timestamp, Uint128, WasmMsg, +}; +use cw20::Cw20Coin; +use skip::{ + asset::Asset, + entry_point::{Action, Affiliate, ExecuteMsg}, + swap::{Swap, SwapExactAssetIn, SwapOperation}, +}; +use skip_go_entry_point::{error::ContractError, state::RECOVER_TEMP_STORAGE}; +use test_case::test_case; +*/ + +/* +Test Cases: + +Expect Response + - Happy Path Single Coin + - Happy Path Multiple Coins + - Happy Path Cw20 Asset + - Sent Asset Not Given With Valid One Coin + - Sent Asset Not Given With Invalid One Coin + + // Note: The following test case is an invalid call to the contract + // showing that under the circumstance both coins and a Cw20 token + // is sent to the contract, the contract will recover all assets. + - Happy Path Multiple Coins And Cw20 Asset + +*/ + +/* +// Define test parameters +struct Params { + info_funds: Vec, + sent_asset: Option, + user_swap: Swap, + min_asset: Asset, + timeout_timestamp: u64, + post_swap_action: Action, + affiliates: Vec, + expected_assets: Vec, + expected_messages: Vec, + expected_error: Option, +} + +// Test execute_swap_and_action_with_recover +#[test_case( + Params { + info_funds: vec![Coin::new(1_000_000, "untrn")], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "untrn"))), + user_swap: Swap::SwapExactAssetIn(SwapExactAssetIn { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + }], + }), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + expected_assets: vec![Asset::Native(Coin::new(1_000_000, "untrn"))], + expected_messages: vec![SubMsg { + id: 1, + msg: CosmosMsg::from(WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::SwapAndAction { + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "untrn"))), + user_swap: Swap::SwapExactAssetIn(SwapExactAssetIn { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + }], + }), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + }) + .unwrap(), + funds: vec![Coin::new(1000000, "untrn")], + }), + gas_limit: None, + reply_on: ReplyOn::Always, + }], + expected_error: None, + }; + "Happy Path Single Coin")] +#[test_case( + Params { + info_funds: vec![Coin::new(1_000_000, "untrn"), Coin::new(1_000_000, "osmo")], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "untrn"))), + user_swap: Swap::SwapExactAssetIn(SwapExactAssetIn { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + }], + }), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + expected_assets: vec![Asset::Native(Coin::new(1_000_000, "untrn")), Asset::Native(Coin::new(1_000_000, "osmo"))], + expected_messages: vec![SubMsg { + id: 1, + msg: CosmosMsg::from(WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::SwapAndAction { + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "untrn"))), + user_swap: Swap::SwapExactAssetIn(SwapExactAssetIn { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + }], + }), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + }) + .unwrap(), + funds: vec![Coin::new(1000000, "untrn"), Coin::new(1000000, "osmo")], + }), + gas_limit: None, + reply_on: ReplyOn::Always, + }], + expected_error: None, + }; + "Happy Path Multiple Coins")] +#[test_case( + Params { + info_funds: vec![], + sent_asset: Some(Asset::Cw20(Cw20Coin{ + address: "neutron123".to_string(), + amount: Uint128::from(1_000_000u128), + })), + user_swap: Swap::SwapExactAssetIn(SwapExactAssetIn { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![SwapOperation { + pool: "pool".to_string(), + denom_in: "neutron123".to_string(), + denom_out: "osmo".to_string(), + interface: None, + }], + }), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + expected_assets: vec![Asset::Cw20(Cw20Coin{ + address: "neutron123".to_string(), + amount: Uint128::from(1_000_000u128), + })], + expected_messages: vec![SubMsg { + id: 1, + msg: CosmosMsg::from(WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::SwapAndAction { + sent_asset: Some(Asset::Cw20(Cw20Coin{ + address: "neutron123".to_string(), + amount: Uint128::from(1_000_000u128), + })), + user_swap: Swap::SwapExactAssetIn(SwapExactAssetIn { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![SwapOperation { + pool: "pool".to_string(), + denom_in: "neutron123".to_string(), + denom_out: "osmo".to_string(), + interface: None, + }], + }), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + }) + .unwrap(), + funds: vec![], + }), + gas_limit: None, + reply_on: ReplyOn::Always, + }], + expected_error: None, + }; + "Happy Path Cw20 Asset")] +#[test_case( + Params { + info_funds: vec![Coin::new(1_000_000, "untrn")], + sent_asset: None, + user_swap: Swap::SwapExactAssetIn(SwapExactAssetIn { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + }], + }), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + expected_assets: vec![Asset::Native(Coin::new(1_000_000, "untrn"))], + expected_messages: vec![SubMsg { + id: 1, + msg: CosmosMsg::from(WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::SwapAndAction { + sent_asset: None, + user_swap: Swap::SwapExactAssetIn(SwapExactAssetIn { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + }], + }), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + }) + .unwrap(), + funds: vec![Coin::new(1000000, "untrn")], + }), + gas_limit: None, + reply_on: ReplyOn::Always, + }], + expected_error: None, + }; + "Sent Asset Not Given With Valid One Coin")] +#[test_case( + Params { + info_funds: vec![Coin::new(1_000_000, "untrn"), Coin::new(1_000_000, "osmo")], + sent_asset: None, + user_swap: Swap::SwapExactAssetIn(SwapExactAssetIn { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + }], + }), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + expected_assets: vec![Asset::Native(Coin::new(1_000_000, "untrn")), Asset::Native(Coin::new(1_000_000, "osmo"))], + expected_messages: vec![SubMsg { + id: 1, + msg: CosmosMsg::from(WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::SwapAndAction { + sent_asset: None, + user_swap: Swap::SwapExactAssetIn(SwapExactAssetIn { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + }], + }), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + }) + .unwrap(), + funds: vec![Coin::new(1000000, "untrn"), Coin::new(1000000, "osmo")], + }), + gas_limit: None, + reply_on: ReplyOn::Always, + }], + expected_error: None, + }; + "Sent Asset Not Given With Invalid One Coin")] +#[test_case( + Params { + info_funds: vec![Coin::new(1_000_000, "untrn"), Coin::new(1_000_000, "osmo")], + sent_asset: Some(Asset::Cw20(Cw20Coin{ + address: "neutron123".to_string(), + amount: Uint128::from(1_000_000u128), + })), + user_swap: Swap::SwapExactAssetIn(SwapExactAssetIn { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + }], + }), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + expected_assets: vec![Asset::Native(Coin::new(1_000_000, "untrn")), Asset::Native(Coin::new(1_000_000, "osmo")), Asset::Cw20(Cw20Coin{ + address: "neutron123".to_string(), + amount: Uint128::from(1_000_000u128), + })], + expected_messages: vec![SubMsg { + id: 1, + msg: CosmosMsg::from(WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_json_binary(&ExecuteMsg::SwapAndAction { + sent_asset: Some(Asset::Cw20(Cw20Coin{ + address: "neutron123".to_string(), + amount: Uint128::from(1_000_000u128), + })), + user_swap: Swap::SwapExactAssetIn(SwapExactAssetIn { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + interface: None, + }], + }), + min_asset: Asset::Native(Coin::new(1_000_000, "osmo")), + timeout_timestamp: 101, + post_swap_action: Action::Transfer { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + }) + .unwrap(), + funds: vec![Coin::new(1000000, "untrn"), Coin::new(1000000, "osmo")], + }), + gas_limit: None, + reply_on: ReplyOn::Always, + }], + expected_error: None, + }; + "Happy Path Multiple Coins And Cw20 Asset")] +fn test_execute_swap_and_action_with_recover(params: Params) { + // Create mock dependencies + let mut deps = mock_dependencies_with_balances(&[( + "entry_point", + &[Coin::new(1_000_000, "osmo"), Coin::new(1_000_000, "untrn")], + )]); + + // Create mock env with parameters that make testing easier + let mut env = mock_env(); + env.contract.address = Addr::unchecked("entry_point"); + env.block.time = Timestamp::from_nanos(100); + + // Convert info funds vector into a slice of Coin objects + let info_funds: &[Coin] = ¶ms.info_funds; + + // Create mock info with entry point contract address + let info = mock_info("swapper", info_funds); + + let recovery_addr = Addr::unchecked("recovery_address"); + // Call execute_swap_and_action with the given test case params + let res = skip_go_entry_point::contract::execute( + deps.as_mut(), + env, + info, + ExecuteMsg::SwapAndActionWithRecover { + sent_asset: params.sent_asset, + user_swap: params.user_swap, + min_asset: params.min_asset, + timeout_timestamp: params.timeout_timestamp, + post_swap_action: params.post_swap_action, + affiliates: params.affiliates, + recovery_addr: recovery_addr.clone(), + }, + ); + + match res { + Ok(res) => { + // Assert the test did not expect an error + assert!( + params.expected_error.is_none(), + "expected test to error with {:?}, but it succeeded", + params.expected_error + ); + + // Assert the number of messages in the response is correct + assert_eq!( + res.messages.len(), + params.expected_messages.len(), + "expected {:?} messages, but got {:?}", + params.expected_messages.len(), + res.messages.len() + ); + + // Assert the messages in the response are correct + assert_eq!(res.messages, params.expected_messages,); + + // Assert the recover temp storage is correct + let recover_temp_storage = RECOVER_TEMP_STORAGE.load(&deps.storage).unwrap(); + assert_eq!(recover_temp_storage.recovery_addr, recovery_addr); + assert_eq!(recover_temp_storage.assets, params.expected_assets); + } + Err(err) => { + // Assert the test expected an error + assert!( + params.expected_error.is_some(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err, params.expected_error.unwrap()); + } + } +} +*/ diff --git a/contracts/secret-entry-point/tests/test_instantiate.rs b/contracts/secret-entry-point/tests/test_instantiate.rs new file mode 100644 index 00000000..a8d3f682 --- /dev/null +++ b/contracts/secret-entry-point/tests/test_instantiate.rs @@ -0,0 +1,148 @@ +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env, mock_info}, + Addr, ContractInfo, +}; +use skip_go_secret_entry_point::{ + error::ContractError, + msg::{InstantiateMsg, SwapVenue}, + state::{BLOCKED_CONTRACT_ADDRESSES, IBC_TRANSFER_CONTRACT, SWAP_VENUE_MAP}, +}; +use test_case::test_case; + +/* +Test Cases: + +Expect Response + - Happy Path (tests the adapter and blocked contract addresses are stored correctly) + +Expect Error + - Duplicate Swap Venue Names + */ + +// Define test parameters +struct Params { + swap_venues: Vec, + ibc_transfer_contract: ContractInfo, + viewing_key: String, + expected_error: Option, +} + +// Test instantiate +#[test_case( + Params { + swap_venues: vec![ + SwapVenue { + name: "shade-swap".to_string(), + adapter_contract: ContractInfo { + address: Addr::unchecked("secret123".to_string()), + code_hash: "code_hash".to_string(), + }, + }, + ], + ibc_transfer_contract: ContractInfo { + address: Addr::unchecked("ibc_transfer_adapter".to_string()), + code_hash: "code_hash".to_string(), + }, + viewing_key: "viewing_key".to_string(), + expected_error: None, + }; + "Happy Path")] +#[test_case( + Params { + swap_venues: vec![ + SwapVenue { + name: "shade-swap".to_string(), + adapter_contract: ContractInfo { + address: Addr::unchecked("secret123".to_string()), + code_hash: "code_hash".to_string(), + }, + }, + SwapVenue { + name: "shade-swap".to_string(), + adapter_contract: ContractInfo { + address: Addr::unchecked("secret123".to_string()), + code_hash: "code_hash".to_string(), + }, + }, + ], + ibc_transfer_contract: ContractInfo { + address: Addr::unchecked("ibc_transfer_adapter".to_string()), + code_hash: "code_hash".to_string(), + }, + viewing_key: "viewing_key".to_string(), + expected_error: Some(ContractError::DuplicateSwapVenueName), + }; + "Duplicate Swap Venue Names")] +fn test_instantiate(params: Params) { + // Create mock dependencies + let mut deps = mock_dependencies(); + + // Create mock info + let info = mock_info("creator", &[]); + + // Create mock env with the entry point contract address + let mut env = mock_env(); + env.contract.address = Addr::unchecked("entry_point"); + + // Call instantiate with the given test parameters + let res = skip_go_secret_entry_point::contract::instantiate( + deps.as_mut(), + env, + info, + InstantiateMsg { + swap_venues: params.swap_venues.clone(), + ibc_transfer_contract: params.ibc_transfer_contract, + viewing_key: params.viewing_key, + }, + ); + + match res { + Ok(_) => { + // Assert the test did not expect an error + assert!( + params.expected_error.is_none(), + "expected test to error with {:?}, but it succeeded", + params.expected_error + ); + + // Assert the entry point contract address exists in the blocked contract addresses map + assert!(BLOCKED_CONTRACT_ADDRESSES + .has(deps.as_ref().storage, &Addr::unchecked("entry_point"))); + + // Get stored ibc transfer adapter contract address + let stored_ibc_transfer_contract = + IBC_TRANSFER_CONTRACT.load(deps.as_ref().storage).unwrap(); + + // Assert the ibc transfer adapter contract address exists in the blocked contract addresses map + assert!(BLOCKED_CONTRACT_ADDRESSES + .has(deps.as_ref().storage, &stored_ibc_transfer_contract.address)); + + params.swap_venues.into_iter().for_each(|swap_venue| { + // Get stored swap venue adapter contract address + let stored_swap_venue_contract = SWAP_VENUE_MAP + .may_load(deps.as_ref().storage, &swap_venue.name) + .unwrap() + .unwrap(); + + // Assert the swap venue name exists in the map and that + // the adapter contract address stored is correct + assert_eq!(&stored_swap_venue_contract, &swap_venue.adapter_contract); + + // Assert the swap adapter contract address exists in the blocked contract addresses map + assert!(BLOCKED_CONTRACT_ADDRESSES + .has(deps.as_ref().storage, &stored_swap_venue_contract.address)); + }); + } + Err(err) => { + // Assert the test expected an error + assert!( + params.expected_error.is_some(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err, params.expected_error.unwrap()); + } + } +} diff --git a/contracts/secret-entry-point/tests/test_reply.rs b/contracts/secret-entry-point/tests/test_reply.rs new file mode 100644 index 00000000..83110207 --- /dev/null +++ b/contracts/secret-entry-point/tests/test_reply.rs @@ -0,0 +1,189 @@ +use cosmwasm_std::{ + testing::{mock_dependencies_with_balances, mock_env}, + to_binary, Addr, BankMsg, Coin, ContractInfo, CosmosMsg, Reply, StdError, SubMsg, + SubMsgResponse, SubMsgResult, Uint128, WasmMsg, +}; +use secret_skip::{asset::Asset, cw20::Cw20Coin, snip20}; +use skip_go_secret_entry_point::{ + reply::RecoverTempStorage, + state::{RECOVER_TEMP_STORAGE, REGISTERED_TOKENS}, +}; +use test_case::test_case; + +/* +Test Cases: + +Expect Response + - Native Asset Sent On Error + - Native Asset Not Sent On Success + - Cw20 Asset Sent On Error + - Cw20 Asset Not Sent On Success + - Native And Cw20 Asset Sent On Error + +Expect Error + - Verify error on invalid reply id + +*/ + +// Define test parameters +struct Params { + reply: Reply, + expected_error_string: String, + storage: Option, + expected_messages: Vec, +} + +// Test reply +#[test_case( + Params { + reply: Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: None, + }), + }, + expected_error_string: "".to_string(), + storage: Some(RecoverTempStorage { + assets: vec![Asset::Native(Coin::new(1_000_000, "osmo"))], + recovery_addr: Addr::unchecked("recovery_addr"), + }), + expected_messages: vec![], + }; + "Native Asset Funds Not Sent On Success" +)] +#[test_case( + Params { + reply: Reply { + id: 1, + result: SubMsgResult::Err("Anything".to_string()), + }, + expected_error_string: "".to_string(), + storage: Some(RecoverTempStorage { + assets: vec![Asset::Cw20(Cw20Coin { + address: "secret123".to_string(), + amount: 1_000_000u128.into(), + })], + recovery_addr: Addr::unchecked("recovery_addr"), + }), + expected_messages: vec![SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "secret123".to_string(), + code_hash: "code_hash".to_string(), + msg: to_binary(&snip20::ExecuteMsg::Transfer { + recipient: "recovery_addr".to_string(), + amount: 1_000_000u128.into(), + memo: None, + padding: None, + }).unwrap(), + funds: vec![], + }))], + }; + "Cw20 Asset Sent On Error" +)] +#[test_case( + Params { + reply: Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: None, + }), + }, + expected_error_string: "".to_string(), + storage: Some(RecoverTempStorage { + assets: vec![Asset::Cw20(Cw20Coin{ + address: "secret123".to_string(), + amount: 1_000_000u128.into(), + })], + recovery_addr: Addr::unchecked("recovery_addr"), + }), + expected_messages: vec![], + }; + "Cw20 Asset Not Sent On Success" +)] +#[test_case( + Params { + reply: Reply { + id: 2, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: None, + }), + }, + expected_error_string: "Reply id: 2 not valid".to_string(), + storage: None, + expected_messages: vec![], + }; + "Verify error on invalid reply id" +)] +fn test_reply(params: Params) { + // Create mock dependencies + let mut deps = mock_dependencies_with_balances(&[( + "entry_point", + &[Coin::new(1_000_000, "osmo"), Coin::new(1_000_000, "untrn")], + )]); + + // Create mock env + let env = mock_env(); + + // Update storage + if let Some(recover_temp_storage) = params.storage.clone() { + RECOVER_TEMP_STORAGE + .save(deps.as_mut().storage, &recover_temp_storage) + .unwrap(); + } + + REGISTERED_TOKENS + .save( + deps.as_mut().storage, + Addr::unchecked("secret123"), + &ContractInfo { + address: Addr::unchecked("secret123"), + code_hash: "code_hash".to_string(), + }, + ) + .unwrap(); + + // Call reply with the given test parameters + let res = skip_go_secret_entry_point::contract::reply(deps.as_mut(), env, params.reply); + + // Assert the behavior is correct + match res { + Ok(res) => { + // Assert the test did not expect an error + assert!( + params.expected_error_string.is_empty(), + "expected test to error with {:?}, but it succeeded", + params.expected_error_string + ); + + assert_eq!(res.messages, params.expected_messages); + + // Verify the in progress recover temp storage was removed from storage + match RECOVER_TEMP_STORAGE.load(&deps.storage) { + Ok(recover_temp_storage) => { + panic!( + "expected in progress recover_temp_storage to be removed: {:?}", + recover_temp_storage + ) + } + Err(err) => assert!( + matches!(err, StdError::NotFound { .. }), + "unexpected error: {:?}", + err + ), + }; + } + Err(err) => { + // Assert the test expected an error + assert!( + !params.expected_error_string.is_empty(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err.to_string(), params.expected_error_string); + } + } +} diff --git a/contracts/secret-entry-point/tests/test_user_swap.rs b/contracts/secret-entry-point/tests/test_user_swap.rs new file mode 100644 index 00000000..b3b80583 --- /dev/null +++ b/contracts/secret-entry-point/tests/test_user_swap.rs @@ -0,0 +1,1282 @@ +/* +use cosmwasm_std::{ + testing::{mock_dependencies_with_balances, mock_env, mock_info}, + to_json_binary, Addr, BankMsg, Coin, ContractResult, OverflowError, OverflowOperation, + QuerierResult, + ReplyOn::Never, + SubMsg, SystemResult, Timestamp, Uint128, WasmMsg, WasmQuery, +}; +use cw20::{Cw20Coin, Cw20ExecuteMsg}; +use skip::{ + asset::Asset, + entry_point::{Affiliate, ExecuteMsg}, + error::SkipError::{ + Overflow, SwapOperationsAssetInDenomMismatch, SwapOperationsAssetOutDenomMismatch, + SwapOperationsEmpty, + }, + swap::{ + ExecuteMsg as SwapExecuteMsg, Route, SmartSwapExactAssetIn, Swap, SwapExactAssetIn, + SwapExactAssetOut, SwapOperation, + }, +}; +use skip_go_entry_point::{error::ContractError, state::SWAP_VENUE_MAP}; +use test_case::test_case; +*/ + +/* +Test Cases: + +Expect Response + // Swap Exact Coin In + - User Swap Exact Coin In With No Affiliates + - User Swap Exact Coin In With Single Affiliate + - User Swap Exact Coin In With Multiple Affiliates + - User Swap Exact Coin In With Zero Fee Affiliate + - User Swap Exact Cw20 Asset In With Single Affiliate + + // Swap Exact Coin Out + - User Swap Exact Coin Out With No Affiliates + - User Swap Exact Coin Out With Single Affiliate + - User Swap Exact Coin Out With Multiple Affiliates + - User Swap Exact Coin Out With Zero Fee Affiliate + - User Swap Exact Coin Out With Refund Amount Zero (Ensure No Refund Message Included) + - User Swap Exact Cw20 Asset Out With Single Affiliate + +Expect Error + // Swap Exact Coin In + - User Swap Exact Coin In First Swap Operation Denom In Is Not The Same As Remaining Coin Received Denom + - User Swap Exact Coin In Last Swap Operation Denom Out Is Not The Same As Min Coin Out Denom + - User Swap Exact Coin In Empty Swap Operations + + // Swap Exact Coin Out + - User Swap Exact Coin Out First Swap Operation Denom In Is Not The Same As Remaining Coin Received Denom + - User Swap Exact Coin Out Last Swap Operation Denom Out Is Not The Same As Min Coin Out Denom + - User Swap Exact Coin Out Empty Swap Operations + - User Swap Exact Coin Out With No Refund Address + - User Swap Exact Coin Out Where Coin In Denom Is Not The Same As Remaining Coin Received Denom + - User Swap Exact Coin Out Where Coin In Amount More Than Remaining Coin Received Amount + - User Swap Exact Asset Out Where Asset In Amount More Than Remaining Asset Received Amount + + // General + - Unauthorized Caller + + */ + +/* +// Define test parameters +struct Params { + caller: String, + user_swap: Swap, + remaining_asset: Asset, + min_asset: Asset, + affiliates: Vec, + expected_messages: Vec, + expected_error: Option, +} + +// Test execute_swap_and_action +#[test_case( + Params { + caller: "entry_point".to_string(), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + } + ), + remaining_asset: Asset::Native(Coin::new(1_000_000, "un")), + min_asset: Asset::Native(Coin::new(1_000_000, "os")), + affiliates: vec![], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_venue_adapter".to_string(), + msg: to_json_binary(&SwapExecuteMsg::Swap { + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + }).unwrap(), + funds: vec![Coin::new(1_000_000, "un")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "User Swap Exact Coin In With No Affiliates")] +#[test_case( + Params { + caller: "entry_point".to_string(), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + } + ), + remaining_asset: Asset::Native(Coin::new(1_000_000, "un")), + min_asset: Asset::Native(Coin::new(1_000_000, "os")), + affiliates: vec![Affiliate { + address: "affiliate".to_string(), + basis_points_fee: Uint128::new(1000), + }], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_venue_adapter".to_string(), + msg: to_json_binary(&SwapExecuteMsg::Swap { + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + }).unwrap(), + funds: vec![Coin::new(1_000_000, "un")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "affiliate".to_string(), + amount: vec![Coin::new(100_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "User Swap Exact Coin In With Single Affiliate")] +#[test_case( + Params { + caller: "entry_point".to_string(), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name_2".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "neutron123".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + } + ), + remaining_asset: Asset::Cw20(Cw20Coin { + address: "neutron123".to_string(), + amount: Uint128::new(1_000_000), + }), + min_asset: Asset::Native(Coin::new(1_000_000, "os")), + affiliates: vec![Affiliate { + address: "affiliate".to_string(), + basis_points_fee: Uint128::new(1000), + }], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "neutron123".to_string(), + msg: to_json_binary(&Cw20ExecuteMsg::Send { + contract: "swap_venue_adapter_2".to_string(), + amount: Uint128::new(1_000_000), + msg: to_json_binary(&SwapExecuteMsg::Swap { + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "neutron123".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + }).unwrap(), + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "affiliate".to_string(), + amount: vec![Coin::new(100_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "User Swap Exact Cw20 Asset In With Single Affiliate")] +#[test_case( + Params { + caller: "entry_point".to_string(), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + } + ), + remaining_asset: Asset::Native(Coin::new(1_000_000, "un")), + min_asset: Asset::Native(Coin::new(1_000_000, "os")), + affiliates: vec![ + Affiliate { + address: "affiliate_1".to_string(), + basis_points_fee: Uint128::new(1000), + }, + Affiliate { + address: "affiliate_2".to_string(), + basis_points_fee: Uint128::new(1000), + }, + ], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_venue_adapter".to_string(), + msg: to_json_binary(&SwapExecuteMsg::Swap { + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + }).unwrap(), + funds: vec![Coin::new(1_000_000, "un")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "affiliate_1".to_string(), + amount: vec![Coin::new(100_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "affiliate_2".to_string(), + amount: vec![Coin::new(100_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "User Swap Exact Coin In With Multiple Affiliates")] +#[test_case( + Params { + caller: "entry_point".to_string(), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + } + ), + remaining_asset: Asset::Native(Coin::new(1_000_000, "un")), + min_asset: Asset::Native(Coin::new(1_000_000, "os")), + affiliates: vec![Affiliate { + address: "affiliate".to_string(), + basis_points_fee: Uint128::new(0), + }], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_venue_adapter".to_string(), + msg: to_json_binary(&SwapExecuteMsg::Swap { + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + }).unwrap(), + funds: vec![Coin::new(1_000_000, "un")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "User Swap Exact Coin In With Zero Fee Affiliate")] +#[test_case( + Params { + caller: "entry_point".to_string(), + user_swap: Swap::SwapExactAssetOut ( + SwapExactAssetOut{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + refund_address: Some("refund_address".to_string()), + } + ), + remaining_asset: Asset::Native(Coin::new(1_000_000, "un")), + min_asset: Asset::Native(Coin::new(500_000, "os")), + affiliates: vec![], + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "refund_address".to_string(), + amount: vec![Coin::new(500_000, "un")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_venue_adapter".to_string(), + msg: to_json_binary(&SwapExecuteMsg::Swap { + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + }).unwrap(), + funds: vec![Coin::new(500_000, "un")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "User Swap Exact Coin Out With No Affiliates")] +#[test_case( + Params { + caller: "entry_point".to_string(), + user_swap: Swap::SwapExactAssetOut ( + SwapExactAssetOut{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + refund_address: Some("refund_address".to_string()), + } + ), + remaining_asset: Asset::Native(Coin::new(1_000_000, "un")), + min_asset: Asset::Native(Coin::new(500_000, "os")), + affiliates: vec![ + Affiliate { + address: "affiliate".to_string(), + basis_points_fee: Uint128::new(1000), + }, + ], + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "refund_address".to_string(), + amount: vec![Coin::new(500_000, "un")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_venue_adapter".to_string(), + msg: to_json_binary(&SwapExecuteMsg::Swap { + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + }).unwrap(), + funds: vec![Coin::new(500_000, "un")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "affiliate".to_string(), + amount: vec![Coin::new(50_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "User Swap Exact Coin Out With Single Affiliate")] +#[test_case( + Params { + caller: "entry_point".to_string(), + user_swap: Swap::SwapExactAssetOut ( + SwapExactAssetOut{ + swap_venue_name: "swap_venue_name_2".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "neutron123".to_string(), + denom_out: "neutron987".to_string(), + interface: None, + } + ], + refund_address: Some("refund_address".to_string()), + } + ), + remaining_asset: Asset::Cw20(Cw20Coin { + address: "neutron123".to_string(), + amount: Uint128::new(1_000_000), + }), + min_asset: Asset::Cw20(Cw20Coin { + address: "neutron987".to_string(), + amount: Uint128::new(1_000_000), + }), + affiliates: vec![Affiliate { + address: "affiliate".to_string(), + basis_points_fee: Uint128::new(1000), + }], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "neutron123".to_string(), + msg: to_json_binary(&Cw20ExecuteMsg::Transfer { + recipient: "refund_address".to_string(), + amount: Uint128::new(500_000), + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "neutron123".to_string(), + msg: to_json_binary(&Cw20ExecuteMsg::Send { + contract: "swap_venue_adapter_2".to_string(), + amount: Uint128::new(500_000), + msg: to_json_binary(&SwapExecuteMsg::Swap { + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "neutron123".to_string(), + denom_out: "neutron987".to_string(), + interface: None, + } + ], + }).unwrap(), + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "neutron987".to_string(), + msg: to_json_binary(&Cw20ExecuteMsg::Transfer { + recipient: "affiliate".to_string(), + amount: Uint128::new(100_000), + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "User Swap Exact Cw20 Asset Out With Single Affiliate")] +#[test_case( + Params { + caller: "entry_point".to_string(), + user_swap: Swap::SwapExactAssetOut ( + SwapExactAssetOut{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + refund_address: Some("refund_address".to_string()), + } + ), + remaining_asset: Asset::Native(Coin::new(1_000_000, "un")), + min_asset: Asset::Native(Coin::new(500_000, "os")), + affiliates: vec![ + Affiliate { + address: "affiliate_1".to_string(), + basis_points_fee: Uint128::new(1000), + }, + Affiliate { + address: "affiliate_2".to_string(), + basis_points_fee: Uint128::new(1000), + }, + ], + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "refund_address".to_string(), + amount: vec![Coin::new(500_000, "un")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_venue_adapter".to_string(), + msg: to_json_binary(&SwapExecuteMsg::Swap { + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + }).unwrap(), + funds: vec![Coin::new(500_000, "un")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "affiliate_1".to_string(), + amount: vec![Coin::new(50_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "affiliate_2".to_string(), + amount: vec![Coin::new(50_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "User Swap Exact Coin Out With Multiple Affiliates")] +#[test_case( + Params { + caller: "entry_point".to_string(), + user_swap: Swap::SwapExactAssetOut ( + SwapExactAssetOut{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + refund_address: Some("refund_address".to_string()), + } + ), + remaining_asset: Asset::Native(Coin::new(1_000_000, "un")), + min_asset: Asset::Native(Coin::new(1_000_000, "os")), + affiliates: vec![ + Affiliate { + address: "affiliate".to_string(), + basis_points_fee: Uint128::new(0), + }, + ], + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "refund_address".to_string(), + amount: vec![Coin::new(500_000, "un")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_venue_adapter".to_string(), + msg: to_json_binary(&SwapExecuteMsg::Swap { + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + }).unwrap(), + funds: vec![Coin::new(500_000, "un")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "User Swap Exact Coin Out With Zero Fee Affiliate")] +#[test_case( + Params { + caller: "entry_point".to_string(), + user_swap: Swap::SwapExactAssetOut ( + SwapExactAssetOut{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + refund_address: Some("refund_address".to_string()), + } + ), + remaining_asset: Asset::Native(Coin::new(500_000, "un")), + min_asset: Asset::Native(Coin::new(500_000, "os")), + affiliates: vec![], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_venue_adapter".to_string(), + msg: to_json_binary(&SwapExecuteMsg::Swap { + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + }).unwrap(), + funds: vec![Coin::new(500_000, "un")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "User Swap Exact Coin Out With Refund Amount Zero")] +#[test_case( + Params { + caller: "entry_point".to_string(), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "un".to_string(), + denom_out: "ua".to_string(), + interface: None, + } + ], + }, + ), + remaining_asset: Asset::Native(Coin::new(1_000_000, "uo")), + min_asset: Asset::Native(Coin::new(100_000, "ua")), + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Skip(SwapOperationsAssetInDenomMismatch)), + }; + "User Swap Exact Coin In First Swap Operation Denom In Is Not The Same As Remaining Coin Received Denom - Expect Error")] +#[test_case( + Params { + caller: "entry_point".to_string(), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "uo".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + }, + ), + remaining_asset: Asset::Native(Coin::new(1_000_000, "uo")), + min_asset: Asset::Native(Coin::new(100_000, "ua")), + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Skip(SwapOperationsAssetOutDenomMismatch)), + }; + "User Swap Exact Coin In Last Swap Operation Denom Out Is Not The Same As Min Coin Out Denom - Expect Error")] +#[test_case( + Params { + caller: "entry_point".to_string(), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![], + }, + ), + remaining_asset: Asset::Native(Coin::new(1_000_000, "un")), + min_asset: Asset::Native(Coin::new(1_000_000, "os")), + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Skip(SwapOperationsEmpty)), + }; + "User Swap Exact Coin In Empty Swap Operations - Expect Error")] +#[test_case( + Params { + caller: "entry_point".to_string(), + user_swap: Swap::SwapExactAssetOut ( + SwapExactAssetOut{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "un".to_string(), + denom_out: "ua".to_string(), + interface: None, + } + ], + refund_address: Some("refund_address".to_string()), + }, + ), + remaining_asset: Asset::Native(Coin::new(1_000_000, "uo")), + min_asset: Asset::Native(Coin::new(100_000, "ua")), + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Skip(SwapOperationsAssetInDenomMismatch)), + }; + "User Swap Exact Coin Out First Swap Operation Denom In Is Not The Same As Remaining Coin Received Denom - Expect Error")] +#[test_case( + Params { + caller: "entry_point".to_string(), + user_swap: Swap::SwapExactAssetOut ( + SwapExactAssetOut{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "os".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + refund_address: Some("refund_address".to_string()), + }, + ), + remaining_asset: Asset::Native(Coin::new(1_000_000, "os")), + min_asset: Asset::Native(Coin::new(100_000, "ua")), + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Skip(SwapOperationsAssetOutDenomMismatch)), + }; + "User Swap Exact Coin Out Last Swap Operation Denom Out Is Not The Same As Min Coin Out Denom - Expect Error")] +#[test_case( + Params { + caller: "entry_point".to_string(), + user_swap: Swap::SwapExactAssetOut ( + SwapExactAssetOut{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![], + refund_address: Some("refund_address".to_string()), + }, + ), + remaining_asset: Asset::Native(Coin::new(1_000_000, "un")), + min_asset: Asset::Native(Coin::new(1_000_000, "os")), + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Skip(SwapOperationsEmpty)), + }; + "User Swap Exact Coin Out Empty Swap Operations - Expect Error")] +#[test_case( + Params { + caller: "entry_point".to_string(), + user_swap: Swap::SwapExactAssetOut ( + SwapExactAssetOut{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + refund_address: None, + } + ), + remaining_asset: Asset::Native(Coin::new(1_000_000, "un")), + min_asset: Asset::Native(Coin::new(500_000, "os")), + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::NoRefundAddress), + }; + "User Swap Exact Coin Out With No Refund Address - Expect Error")] +#[test_case( + Params { + caller: "entry_point".to_string(), + user_swap: Swap::SwapExactAssetOut ( + SwapExactAssetOut{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "ua".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + refund_address: Some("refund_address".to_string()), + } + ), + remaining_asset: Asset::Native(Coin::new(1_000_000, "ua")), + min_asset: Asset::Native(Coin::new(500_000, "os")), + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::UserSwapAssetInDenomMismatch), + }; + "User Swap Exact Coin Out Where Coin In Denom Is Not The Same As Remaining Coin Received Denom - Expect Error")] +#[test_case( + Params { + caller: "entry_point".to_string(), + user_swap: Swap::SwapExactAssetOut ( + SwapExactAssetOut{ + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + refund_address: Some("refund_address".to_string()), + } + ), + remaining_asset: Asset::Native(Coin::new(499_999, "un")), + min_asset: Asset::Native(Coin::new(500_000, "os")), + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Skip(Overflow(OverflowError { + operation: OverflowOperation::Sub, + operand1: "499999".to_string(), + operand2: "500000".to_string(), + }))), + }; + "User Swap Exact Coin Out Where Coin In Amount More Than Remaining Coin Received Amount - Expect Error")] +#[test_case( + Params { + caller: "entry_point".to_string(), + user_swap: Swap::SwapExactAssetOut ( + SwapExactAssetOut{ + swap_venue_name: "swap_venue_name_2".to_string(), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "neutron123".to_string(), + denom_out: "os".to_string(), + interface: None, + } + ], + refund_address: Some("refund_address".to_string()), + } + ), + remaining_asset: Asset::Cw20(Cw20Coin { + address: "neutron123".to_string(), + amount: Uint128::new(499_999), + }), + min_asset: Asset::Native(Coin::new(500_000, "os")), + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Skip(Overflow(OverflowError { + operation: OverflowOperation::Sub, + operand1: "499999".to_string(), + operand2: "500000".to_string(), + }))), + }; + "User Swap Exact Asset Out Where Asset In Amount More Than Remaining Asset Received Amount - Expect Error")] +#[test_case( + Params { + caller: "random".to_string(), + user_swap: Swap::SwapExactAssetIn ( + SwapExactAssetIn { + swap_venue_name: "swap_venue_name".to_string(), + operations: vec![], + }, + ), + remaining_asset: Asset::Native(Coin::new(1_000_000, "os")), + min_asset: Asset::Native(Coin::new(1_000_000, "os")), + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Unauthorized), + }; + "Unauthorized Caller - Expect Error")] +#[test_case(Params { + caller: "entry_point".to_string(), + user_swap: Swap::SmartSwapExactAssetIn(SmartSwapExactAssetIn { + swap_venue_name: "swap_venue_name".to_string(), + routes: vec![ + Route { + offer_asset: Asset::Native(Coin::new(250_000, "un")), + operations: vec![SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + }], + }, + Route { + offer_asset: Asset::Native(Coin::new(750_000, "un")), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "un".to_string(), + denom_out: "neutron123".to_string(), + interface: None, + }, + SwapOperation { + pool: "pool_3".to_string(), + denom_in: "neutron123".to_string(), + denom_out: "os".to_string(), + interface: None, + }, + ], + }, + ], + }), + remaining_asset: Asset::Native(Coin::new(1_000_000, "un")), + min_asset: Asset::Native(Coin::new(1_000_000, "os")), + affiliates: vec![], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_venue_adapter".to_string(), + msg: to_json_binary(&SwapExecuteMsg::Swap { + operations: vec![SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + }], + }) + .unwrap(), + funds: vec![Coin::new(250_000, "un")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_venue_adapter".to_string(), + msg: to_json_binary(&SwapExecuteMsg::Swap { + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "un".to_string(), + denom_out: "neutron123".to_string(), + interface: None, + }, + SwapOperation { + pool: "pool_3".to_string(), + denom_in: "neutron123".to_string(), + denom_out: "os".to_string(), + interface: None, + }, + ], + }) + .unwrap(), + funds: vec![Coin::new(750_000, "un")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, +}; "SmartSwapExactAssetIn")] +#[test_case(Params { + caller: "entry_point".to_string(), + user_swap: Swap::SmartSwapExactAssetIn(SmartSwapExactAssetIn { + swap_venue_name: "swap_venue_name".to_string(), + routes: vec![ + Route { + offer_asset: Asset::Native(Coin::new(250_000, "un")), + operations: vec![SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + }], + }, + Route { + offer_asset: Asset::Native(Coin::new(750_000, "un")), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "neutron123".to_string(), + denom_out: "un".to_string(), + interface: None, + }, + SwapOperation { + pool: "pool_3".to_string(), + denom_in: "neutron123".to_string(), + denom_out: "os".to_string(), + interface: None, + }, + ], + }, + ], + }), + remaining_asset: Asset::Native(Coin::new(1_000_000, "un")), + min_asset: Asset::Native(Coin::new(1_000_000, "os")), + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Skip(SwapOperationsAssetInDenomMismatch)), +}; "SmartSwapExactAssetIn With Mismatched Denom In - Expect Error")] +#[test_case(Params { + caller: "entry_point".to_string(), + user_swap: Swap::SmartSwapExactAssetIn(SmartSwapExactAssetIn { + swap_venue_name: "swap_venue_name".to_string(), + routes: vec![ + Route { + offer_asset: Asset::Native(Coin::new(250_000, "un")), + operations: vec![SwapOperation { + pool: "pool".to_string(), + denom_in: "un".to_string(), + denom_out: "os".to_string(), + interface: None, + }], + }, + Route { + offer_asset: Asset::Native(Coin::new(750_000, "un")), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "un".to_string(), + denom_out: "neutron123".to_string(), + interface: None, + }, + SwapOperation { + pool: "pool_3".to_string(), + denom_in: "neutron123".to_string(), + denom_out: "oa".to_string(), + interface: None, + }, + ], + }, + ], + }), + remaining_asset: Asset::Native(Coin::new(1_000_000, "un")), + min_asset: Asset::Native(Coin::new(1_000_000, "os")), + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Skip(SwapOperationsAssetOutDenomMismatch)), +}; "SmartSwapExactAssetIn With Mismatched Denom Out - Expect Error")] +fn test_execute_user_swap(params: Params) { + // Create mock dependencies + let mut deps = mock_dependencies_with_balances(&[( + "entry_point", + &[Coin::new(1_000_000, "os"), Coin::new(1_000_000, "un")], + )]); + + // Create mock wasm handler to handle the swap adapter contract query + let wasm_handler = |query: &WasmQuery| -> QuerierResult { + match query { + WasmQuery::Smart { contract_addr, .. } => { + if contract_addr == "swap_venue_adapter" { + SystemResult::Ok(ContractResult::Ok( + to_json_binary(&Asset::Native(Coin::new(500_000, "un"))).unwrap(), + )) + } else { + SystemResult::Ok(ContractResult::Ok( + to_json_binary(&Asset::Cw20(Cw20Coin { + address: "neutron123".to_string(), + amount: Uint128::new(500_000), + })) + .unwrap(), + )) + } + } + _ => panic!("Unsupported query: {:?}", query), + } + }; + + // Update querier with mock wasm handler + deps.querier.update_wasm(wasm_handler); + + // Create mock env with parameters that make testing easier + let mut env = mock_env(); + env.contract.address = Addr::unchecked("entry_point"); + env.block.time = Timestamp::from_nanos(100); + + // Create mock info with entry point contract address + let info = mock_info(¶ms.caller, &[]); + + // Store the swap venue adapter contract address in the swap venue map + let swap_venue_adapter = Addr::unchecked("swap_venue_adapter"); + let swap_venue_adapter_2 = Addr::unchecked("swap_venue_adapter_2"); + SWAP_VENUE_MAP + .save( + deps.as_mut().storage, + "swap_venue_name", + &swap_venue_adapter, + ) + .unwrap(); + SWAP_VENUE_MAP + .save( + deps.as_mut().storage, + "swap_venue_name_2", + &swap_venue_adapter_2, + ) + .unwrap(); + + // Call execute_swap_and_action with the given test case params + let res = skip_go_entry_point::contract::execute( + deps.as_mut(), + env, + info, + ExecuteMsg::UserSwap { + swap: params.user_swap, + remaining_asset: params.remaining_asset, + min_asset: params.min_asset, + affiliates: params.affiliates, + }, + ); + + match res { + Ok(res) => { + // Assert the test did not expect an error + assert!( + params.expected_error.is_none(), + "expected test to error with {:?}, but it succeeded", + params.expected_error + ); + + // Assert the number of messages in the response is correct + assert_eq!( + res.messages.len(), + params.expected_messages.len(), + "expected {:?} messages, but got {:?}", + params.expected_messages.len(), + res.messages.len() + ); + + // Assert the messages in the response are correct + assert_eq!(res.messages, params.expected_messages,); + } + Err(err) => { + // Assert the test expected an error + assert!( + params.expected_error.is_some(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err, params.expected_error.unwrap()); + } + } +} +*/ diff --git a/packages/secret-skip/Cargo.toml b/packages/secret-skip/Cargo.toml new file mode 100644 index 00000000..89412f0d --- /dev/null +++ b/packages/secret-skip/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "secret-skip" +version = { workspace = true } +rust-version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +documentation = { workspace = true } +keywords = { workspace = true } + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] + +[dependencies] +#astroport = { workspace = true } +cosmos-sdk-proto = { workspace = true } +cosmwasm-schema = { workspace = true } +#cosmwasm-std = { workspace = true } +#cw-utils = { workspace = true } +#cw20 = { workspace = true } +ibc-proto = { workspace = true } +#neutron-proto = { workspace = true } +#osmosis-std = { workspace = true } +thiserror = { workspace = true } +#white-whale-std = { workspace = true } + +cosmwasm-std = { package = "secret-cosmwasm-std", version = "1.1.11"} +secret-toolkit = { git = "https://github.com/scrtlabs/secret-toolkit", tag = "v0.10.0" } +secret-storage-plus = { git = "https://github.com/securesecrets/secret-plus-utils", tag = "v0.1.1", features = [] } diff --git a/packages/secret-skip/README.md b/packages/secret-skip/README.md new file mode 100644 index 00000000..b6e903e6 --- /dev/null +++ b/packages/secret-skip/README.md @@ -0,0 +1,4 @@ +# Skip: Common Types and Functions + +This Skip packages folder contains common types and functions that are used across multiple Skip Go contracts. + diff --git a/packages/secret-skip/src/asset.rs b/packages/secret-skip/src/asset.rs new file mode 100644 index 00000000..af8525f4 --- /dev/null +++ b/packages/secret-skip/src/asset.rs @@ -0,0 +1,645 @@ +use crate::cw20::{Cw20Coin, Cw20CoinVerified}; +use crate::error::SkipError; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Api, Coin, OverflowError, OverflowOperation, Uint128}; + +#[cw_serde] +pub enum Asset { + Native(Coin), + Cw20(Cw20Coin), +} + +impl From for Asset { + fn from(coin: Coin) -> Self { + Asset::Native(coin) + } +} + +impl From for Asset { + fn from(cw20_coin: Cw20Coin) -> Self { + Asset::Cw20(cw20_coin) + } +} + +impl From for Asset { + fn from(cw20_coin_verified: Cw20CoinVerified) -> Self { + Asset::Cw20(Cw20Coin { + address: cw20_coin_verified.address.to_string(), + amount: cw20_coin_verified.amount, + }) + } +} + +impl Asset { + pub fn default_native() -> Self { + Asset::Native(Coin::default()) + } + + pub fn new(api: &dyn Api, denom: &str, amount: Uint128) -> Self { + match api.addr_validate(denom) { + Ok(addr) => Asset::Cw20(Cw20Coin { + address: addr.to_string(), + amount: amount.u128().into(), + }), + Err(_) => Asset::Native(Coin { + denom: denom.to_string(), + amount, + }), + } + } + + pub fn denom(&self) -> &str { + match self { + Asset::Native(coin) => &coin.denom, + Asset::Cw20(coin) => &coin.address, + } + } + + pub fn amount(&self) -> Uint128 { + match self { + Asset::Native(coin) => coin.amount.u128().into(), + Asset::Cw20(coin) => coin.amount.u128().into(), + } + } + + pub fn add(&mut self, amount: Uint128) -> Result { + match self { + Asset::Native(coin) => { + coin.amount = coin.amount.checked_add(amount)?; + Ok(coin.amount) + } + Asset::Cw20(coin) => { + coin.amount = (coin.amount.u128() + amount.u128()).into(); + Ok(coin.amount.u128().into()) + } + } + } + + pub fn sub(&mut self, amount: Uint128) -> Result { + match self { + Asset::Native(coin) => { + coin.amount = coin.amount.checked_sub(amount)?; + Ok(coin.amount) + } + Asset::Cw20(coin) => { + if amount > coin.amount.u128().into() { + return Err( + OverflowError::new(OverflowOperation::Sub, amount, coin.amount).into(), + ); + } + coin.amount = (coin.amount.u128() - amount.u128()).into(); + Ok(coin.amount.u128().into()) + } + } + } + + /* + pub fn transfer(self, to_address: &str) -> CosmosMsg { + match self { + Asset::Native(coin) => CosmosMsg::Bank(BankMsg::Send { + to_address: to_address.to_string(), + amount: vec![coin], + }), + Asset::Cw20(coin) => CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: coin.address.clone(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: to_address.to_string(), + amount: coin.amount, + }) + .unwrap(), + funds: vec![], + }), + } + } + + pub fn into_wasm_msg(self, contract_addr: String, msg: Binary) -> Result { + match self { + Asset::Native(coin) => Ok(WasmMsg::Execute { + contract_addr, + msg, + funds: vec![coin], + }), + Asset::Cw20(coin) => Ok(WasmMsg::Execute { + contract_addr: coin.address, + msg: to_binary(&Cw20ExecuteMsg::Send { + contract: contract_addr, + amount: coin.amount, + msg, + })?, + funds: vec![], + }), + } + } + */ + + /* + pub fn into_astroport_asset(&self, api: &dyn Api) -> Result { + match self { + Asset::Native(coin) => Ok(AstroportAsset { + info: AssetInfo::NativeToken { + denom: coin.denom.clone(), + }, + amount: coin.amount, + }), + Asset::Cw20(cw20_coin) => Ok(AstroportAsset { + info: AssetInfo::Token { + contract_addr: api.addr_validate(&cw20_coin.address)?, + }, + amount: cw20_coin.amount, + }), + } + } + + pub fn into_white_whale_asset(&self, api: &dyn Api) -> Result { + match self { + Asset::Native(coin) => Ok(WhiteWhaleAsset { + info: WhiteWhaleAssetInfo::NativeToken { + denom: coin.denom.clone(), + }, + amount: coin.amount, + }), + Asset::Cw20(cw20_coin) => Ok(WhiteWhaleAsset { + info: WhiteWhaleAssetInfo::Token { + contract_addr: api.addr_validate(&cw20_coin.address)?.into_string(), + }, + amount: cw20_coin.amount, + }), + } + } + + pub fn validate(&self, deps: &DepsMut, env: &Env, info: &MessageInfo) -> Result<(), SkipError> { + match self { + Asset::Native(coin) => { + let compare_coin = one_coin(info)?; + + if compare_coin.eq(coin) { + Ok(()) + } else { + Err(SkipError::InvalidNativeCoin) + } + } + Asset::Cw20(coin) => { + // Validate that the message is nonpayable + nonpayable(info)?; + + let verified_cw20_coin_addr = deps.api.addr_validate(&coin.address)?; + + let cw20_contract = Cw20Contract(verified_cw20_coin_addr); + + let balance = cw20_contract.balance(&deps.querier, &env.contract.address)?; + + if coin.amount <= balance { + Ok(()) + } else { + Err(SkipError::InvalidCw20Coin) + } + } + } + } + */ +} + +/* +pub fn get_current_asset_available( + deps: &DepsMut, + env: &Env, + denom: &str, +) -> Result { + match deps.api.addr_validate(denom) { + Ok(addr) => { + let cw20_contract = Cw20Contract(addr.clone()); + + let amount = cw20_contract.balance(&deps.querier, &env.contract.address)?; + + Ok(Asset::Cw20(Cw20Coin { + address: addr.to_string(), + amount, + })) + } + Err(_) => { + let coin = deps.querier.query_balance(&env.contract.address, denom)?; + + Ok(Asset::Native(coin)) + } + } +} +*/ + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::{ + testing::{mock_dependencies_with_balances, mock_env, mock_info}, + Addr, ContractResult, QuerierResult, SystemResult, WasmQuery, + }; + use cw20::BalanceResponse; + + #[test] + fn test_default_native() { + let asset = Asset::default_native(); + + assert_eq!( + asset, + Asset::Native(Coin { + denom: "".to_string(), + amount: Uint128::zero(), + }) + ); + } + + #[test] + fn test_new() { + // TEST 1: Native asset + let mut deps = mock_dependencies_with_balances(&[("entry_point", &[])]); + + let asset = Asset::new(deps.as_mut().api, "ua", Uint128::new(100)); + + assert_eq!( + asset, + Asset::Native(Coin { + denom: "ua".to_string(), + amount: Uint128::new(100), + }) + ); + + // TEST 2: Cw20 asset + let mut deps = mock_dependencies_with_balances(&[("entry_point", &[])]); + + let asset = Asset::new(deps.as_mut().api, "asset", Uint128::new(100)); + + assert_eq!( + asset, + Asset::Cw20(Cw20Coin { + address: "asset".to_string(), + amount: Uint128::new(100), + }) + ); + } + + #[test] + fn test_asset_native() { + let asset = Asset::Native(Coin { + denom: "uatom".to_string(), + amount: Uint128::new(100), + }); + + assert_eq!(asset.denom(), "uatom"); + assert_eq!(asset.amount(), Uint128::new(100)); + } + + #[test] + fn test_asset_cw20() { + let asset = Asset::Cw20(Cw20Coin { + address: "asset".to_string(), + amount: Uint128::new(100), + }); + + assert_eq!(asset.denom(), "asset"); + assert_eq!(asset.amount(), Uint128::new(100)); + } + + #[test] + fn test_add() { + // TEST 1: Native asset + let mut asset = Asset::Native(Coin { + denom: "uatom".to_string(), + amount: Uint128::new(100), + }); + + asset.add(Uint128::new(20)).unwrap(); + + assert_eq!(asset.amount(), Uint128::new(120)); + + // TEST 2: Cw20 asset + let mut asset = Asset::Cw20(Cw20Coin { + address: "asset".to_string(), + amount: Uint128::new(100), + }); + + asset.add(Uint128::new(20)).unwrap(); + + assert_eq!(asset.amount(), Uint128::new(120)); + } + + #[test] + fn test_sub() { + // TEST 1: Native asset + let mut asset = Asset::Native(Coin { + denom: "uatom".to_string(), + amount: Uint128::new(100), + }); + + asset.sub(Uint128::new(20)).unwrap(); + + assert_eq!(asset.amount(), Uint128::new(80)); + + // TEST 2: Cw20 asset + let mut asset = Asset::Cw20(Cw20Coin { + address: "asset".to_string(), + amount: Uint128::new(100), + }); + + asset.sub(Uint128::new(20)).unwrap(); + + assert_eq!(asset.amount(), Uint128::new(80)); + } + + #[test] + fn test_asset_transfer_native() { + let asset = Asset::Native(Coin { + denom: "uatom".to_string(), + amount: Uint128::new(100), + }); + + let msg = asset.transfer("addr"); + + match msg { + CosmosMsg::Bank(BankMsg::Send { to_address, amount }) => { + assert_eq!(to_address, "addr"); + assert_eq!(amount.len(), 1); + assert_eq!(amount[0].denom, "uatom"); + assert_eq!(amount[0].amount, Uint128::new(100)); + } + _ => panic!("Unexpected message type"), + } + } + + #[test] + fn test_asset_transfer_cw20() { + let asset = Asset::Cw20(Cw20Coin { + address: "asset".to_string(), + amount: Uint128::new(100), + }); + + let msg = asset.transfer("addr"); + + match msg { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr, + msg, + funds, + }) => { + assert_eq!(contract_addr, "asset"); + assert_eq!( + msg, + to_binary(&Cw20ExecuteMsg::Transfer { + recipient: "addr".to_string(), + amount: Uint128::new(100), + }) + .unwrap() + ); + assert_eq!(funds.len(), 0); + } + _ => panic!("Unexpected message type"), + } + } + + #[test] + fn test_into_astroport_asset() { + // TEST 1: Native asset + let mut deps = mock_dependencies_with_balances(&[("entry_point", &[])]); + + let asset = Asset::Native(Coin { + denom: "uatom".to_string(), + amount: Uint128::new(100), + }); + + let astroport_asset = asset.into_astroport_asset(deps.as_mut().api).unwrap(); + + assert_eq!( + astroport_asset, + AstroportAsset { + info: AssetInfo::NativeToken { + denom: "uatom".to_string() + }, + amount: Uint128::new(100), + } + ); + + // TEST 2: Cw20 asset + let mut deps = mock_dependencies_with_balances(&[("entry_point", &[])]); + + let asset = Asset::Cw20(Cw20Coin { + address: "asset".to_string(), + amount: Uint128::new(100), + }); + + let astroport_asset = asset.into_astroport_asset(deps.as_mut().api).unwrap(); + + assert_eq!( + astroport_asset, + AstroportAsset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset") + }, + amount: Uint128::new(100), + } + ); + } + + #[test] + fn test_validate_native() { + // TEST 1: Valid asset + let asset = Asset::Native(Coin { + denom: "uatom".to_string(), + amount: Uint128::new(100), + }); + + let mut deps = mock_dependencies_with_balances(&[("entry_point", &[])]); + + let env = mock_env(); + + let info = mock_info( + "sender", + &[Coin { + denom: "uatom".to_string(), + amount: Uint128::new(100), + }], + ); + + assert!(asset.validate(&deps.as_mut(), &env, &info).is_ok()); + + // TEST 2: Invalid asset due to less amount of denom sent + let asset = Asset::Native(Coin { + denom: "uatom".to_string(), + amount: Uint128::new(100), + }); + + let mut deps = mock_dependencies_with_balances(&[("entry_point", &[])]); + + let env = mock_env(); + + let info = mock_info( + "sender", + &[Coin { + denom: "uatom".to_string(), + amount: Uint128::new(50), + }], + ); + + let res = asset.validate(&deps.as_mut(), &env, &info); + + assert_eq!(res, Err(SkipError::InvalidNativeCoin)); + + // TEST 3: Invalid asset due to more than one coin sent + let asset = Asset::Native(Coin { + denom: "uatom".to_string(), + amount: Uint128::new(100), + }); + + let mut deps = mock_dependencies_with_balances(&[("entry_point", &[])]); + + let env = mock_env(); + + let info = mock_info( + "sender", + &[ + Coin { + denom: "uatom".to_string(), + amount: Uint128::new(100), + }, + Coin { + denom: "uosmo".to_string(), + amount: Uint128::new(50), + }, + ], + ); + + let res = asset.validate(&deps.as_mut(), &env, &info); + + assert_eq!(res, Err(SkipError::MultipleDenoms {})); + } + + #[test] + fn test_validate_cw20() { + // TEST 1: Valid asset + let asset = Asset::Cw20(Cw20Coin { + address: "asset".to_string(), + amount: Uint128::new(100), + }); + + // Create mock wasm handler to handle the cw20 balance query + let wasm_handler = |query: &WasmQuery| -> QuerierResult { + match query { + WasmQuery::Smart { .. } => SystemResult::Ok(ContractResult::Ok( + to_binary(&BalanceResponse { + balance: Uint128::from(100u128), + }) + .unwrap(), + )), + _ => panic!("Unsupported query: {:?}", query), + } + }; + + let mut deps = mock_dependencies_with_balances(&[("entry_point", &[])]); + + deps.querier.update_wasm(wasm_handler); + + let env = mock_env(); + + let info = mock_info("sender", &[]); + + assert!(asset.validate(&deps.as_mut(), &env, &info).is_ok()); + + // TEST 2: Invalid asset due to native coin sent in info + let asset = Asset::Cw20(Cw20Coin { + address: "asset".to_string(), + amount: Uint128::new(100), + }); + + let mut deps = mock_dependencies_with_balances(&[("entry_point", &[])]); + + let env = mock_env(); + + let info = mock_info( + "sender", + &[Coin { + denom: "uatom".to_string(), + amount: Uint128::new(100), + }], + ); + + let res = asset.validate(&deps.as_mut(), &env, &info); + + assert_eq!(res, Err(SkipError::NonPayable {})); + + // TEST 3: Invalid asset due to invalid cw20 balance + let asset = Asset::Cw20(Cw20Coin { + address: "asset".to_string(), + amount: Uint128::new(100), + }); + + // Create mock wasm handler to handle the cw20 balance query + let wasm_handler = |query: &WasmQuery| -> QuerierResult { + match query { + WasmQuery::Smart { .. } => SystemResult::Ok(ContractResult::Ok( + to_binary(&BalanceResponse { + balance: Uint128::from(50u128), + }) + .unwrap(), + )), + _ => panic!("Unsupported query: {:?}", query), + } + }; + + let mut deps = mock_dependencies_with_balances(&[("entry_point", &[])]); + + deps.querier.update_wasm(wasm_handler); + + let env = mock_env(); + + let info = mock_info("sender", &[]); + + let res = asset.validate(&deps.as_mut(), &env, &info); + + assert_eq!(res, Err(SkipError::InvalidCw20Coin)); + } + + #[test] + fn test_get_current_asset_available() { + // TEST 1: Native asset + let mut deps = mock_dependencies_with_balances(&[("entry_point", &[Coin::new(100, "ua")])]); + + let mut env = mock_env(); + env.contract.address = Addr::unchecked("entry_point"); + + let asset = get_current_asset_available(&deps.as_mut(), &env, "ua").unwrap(); + + assert_eq!( + asset, + Asset::Native(Coin { + denom: "ua".to_string(), + amount: Uint128::new(100), + }) + ); + + // TEST 2: Cw20 asset + let mut deps = mock_dependencies_with_balances(&[("entry_point", &[])]); + + let wasm_handler = |query: &WasmQuery| -> QuerierResult { + match query { + WasmQuery::Smart { .. } => SystemResult::Ok(ContractResult::Ok( + to_binary(&BalanceResponse { + balance: Uint128::from(100u128), + }) + .unwrap(), + )), + _ => panic!("Unsupported query: {:?}", query), + } + }; + + deps.querier.update_wasm(wasm_handler); + + let env = mock_env(); + + let asset = get_current_asset_available(&deps.as_mut(), &env, "asset").unwrap(); + + assert_eq!( + asset, + Asset::Cw20(Cw20Coin { + address: "asset".to_string(), + amount: Uint128::new(100), + }) + ); + } +} diff --git a/packages/secret-skip/src/cw20.rs b/packages/secret-skip/src/cw20.rs new file mode 100644 index 00000000..09d461ec --- /dev/null +++ b/packages/secret-skip/src/cw20.rs @@ -0,0 +1,39 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Uint128}; +use std::fmt; + +#[cw_serde] +pub struct Cw20Coin { + pub address: String, + pub amount: Uint128, +} + +impl Cw20Coin { + pub fn is_empty(&self) -> bool { + self.amount == Uint128::zero() + } +} + +impl fmt::Display for Cw20Coin { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "address: {}, amount: {}", self.address, self.amount) + } +} + +#[cw_serde] +pub struct Cw20CoinVerified { + pub address: Addr, + pub amount: Uint128, +} + +impl Cw20CoinVerified { + pub fn is_empty(&self) -> bool { + self.amount == Uint128::zero() + } +} + +impl fmt::Display for Cw20CoinVerified { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "address: {}, amount: {}", self.address, self.amount) + } +} diff --git a/packages/secret-skip/src/entry_point.rs b/packages/secret-skip/src/entry_point.rs new file mode 100644 index 00000000..5642e786 --- /dev/null +++ b/packages/secret-skip/src/entry_point.rs @@ -0,0 +1,172 @@ +use crate::{ + asset::Asset, + ibc::IbcInfo, + snip20::Snip20ReceiveMsg, + swap::{Swap, SwapExactAssetOut, SwapVenue}, +}; + +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Binary, HexBinary, Uint128}; + +/////////////// +/// MIGRATE /// +/////////////// + +// The MigrateMsg struct defines the migration parameters for the entry point contract. +#[cw_serde] +pub struct MigrateMsg {} + +/////////////////// +/// INSTANTIATE /// +/////////////////// + +// The InstantiateMsg struct defines the initialization parameters for the entry point contract. +#[cw_serde] +pub struct InstantiateMsg { + pub swap_venues: Vec, + pub ibc_transfer_contract_address: String, + pub hyperlane_transfer_contract_address: Option, +} + +/////////////// +/// EXECUTE /// +/////////////// + +// The ExecuteMsg enum defines the execution messages that the entry point contract can handle. +// Only the SwapAndAction message is callable by external users. +#[cw_serde] +#[allow(clippy::large_enum_variant)] +pub enum ExecuteMsg { + Receive(Snip20ReceiveMsg), + SwapAndActionWithRecover { + sent_asset: Option, + user_swap: Swap, + min_asset: Asset, + timeout_timestamp: u64, + post_swap_action: Action, + affiliates: Vec, + recovery_addr: Addr, + }, + SwapAndAction { + sent_asset: Option, + user_swap: Swap, + min_asset: Asset, + timeout_timestamp: u64, + post_swap_action: Action, + affiliates: Vec, + }, + UserSwap { + swap: Swap, + min_asset: Asset, + remaining_asset: Asset, + affiliates: Vec, + }, + PostSwapAction { + min_asset: Asset, + timeout_timestamp: u64, + post_swap_action: Action, + exact_out: bool, + }, + Action { + sent_asset: Option, + timeout_timestamp: u64, + action: Action, + exact_out: bool, + min_asset: Option, + }, + ActionWithRecover { + sent_asset: Option, + timeout_timestamp: u64, + action: Action, + exact_out: bool, + min_asset: Option, + recovery_addr: Addr, + }, +} + +/// This structure describes a CW20 hook message. +#[cw_serde] +pub enum Cw20HookMsg { + SwapAndActionWithRecover { + user_swap: Swap, + min_asset: Asset, + timeout_timestamp: u64, + post_swap_action: Action, + affiliates: Vec, + recovery_addr: Addr, + }, + SwapAndAction { + user_swap: Swap, + min_asset: Asset, + timeout_timestamp: u64, + post_swap_action: Action, + affiliates: Vec, + }, + Action { + timeout_timestamp: u64, + action: Action, + exact_out: bool, + min_asset: Option, + }, + ActionWithRecover { + timeout_timestamp: u64, + action: Action, + exact_out: bool, + min_asset: Option, + recovery_addr: Addr, + }, +} + +///////////// +/// QUERY /// +///////////// + +// The QueryMsg enum defines the queries the entry point contract provides. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + // SwapVenueAdapterContract returns the address of the swap + // adapter contract for the given swap venue name. + #[returns(cosmwasm_std::Addr)] + SwapVenueAdapterContract { name: String }, + + // IbcTransferAdapterContract returns the address of the IBC + // transfer adapter contract. + #[returns(cosmwasm_std::Addr)] + IbcTransferAdapterContract {}, +} + +//////////////////// +/// COMMON TYPES /// +//////////////////// + +// The Action enum is used to specify what action to take after a swap. +#[cw_serde] +pub enum Action { + Transfer { + to_address: String, + }, + IbcTransfer { + ibc_info: IbcInfo, + fee_swap: Option, + }, + ContractCall { + contract_address: String, + msg: Binary, + }, + HplTransfer { + dest_domain: u32, + recipient: HexBinary, + hook: Option, + metadata: Option, + warp_address: String, + }, +} + +// The Affiliate struct is used to specify an affiliate address and BPS fee taken +// from the min_asset to send to that address. +#[cw_serde] +pub struct Affiliate { + pub basis_points_fee: Uint128, + pub address: String, +} diff --git a/packages/secret-skip/src/error.rs b/packages/secret-skip/src/error.rs new file mode 100644 index 00000000..ab6d1403 --- /dev/null +++ b/packages/secret-skip/src/error.rs @@ -0,0 +1,61 @@ +use cosmwasm_std::{OverflowError, StdError}; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum SkipError { + /////////////// + /// GENERAL /// + /////////////// + + #[error(transparent)] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized, + + /* + #[error(transparent)] + Payment(#[from] PaymentError), + */ + #[error("Multiple Denoms")] + MultipleDenoms {}, + + #[error("NonPayable")] + NonPayable {}, + + #[error(transparent)] + Overflow(#[from] OverflowError), + + //////////// + /// SWAP /// + //////////// + + #[error("Swap Operations Empty")] + SwapOperationsEmpty, + + #[error("First Swap Operations' Denom In Differs From Swap Asset In Denom")] + SwapOperationsAssetInDenomMismatch, + + #[error("Last Swap Operations' Denom Out Differs From Swap Asset Out Denom")] + SwapOperationsAssetOutDenomMismatch, + + #[error("Routes Empty")] + RoutesEmpty, + + /////////// + /// IBC /// + /////////// + + #[error("Ibc Fees Are Not A Single Coin, Either Multiple Denoms Or No Coin Specified")] + IbcFeesNotOneCoin, + + ///////////// + /// ASSET /// + ///////////// + + #[error("Native Coin Sent To Contract Does Not Match Asset")] + InvalidNativeCoin, + + #[error("Cw20 Coin Sent To Contract Does Not Match Asset")] + InvalidCw20Coin, +} diff --git a/packages/secret-skip/src/hyperlane.rs b/packages/secret-skip/src/hyperlane.rs new file mode 100644 index 00000000..65506a5b --- /dev/null +++ b/packages/secret-skip/src/hyperlane.rs @@ -0,0 +1,37 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::HexBinary; + +/////////////// +/// MIGRATE /// +/////////////// + +// The MigrateMsg struct defines the migration parameters used. +#[cw_serde] +pub struct MigrateMsg { + pub entry_point_contract_address: String, +} +/////////////////// +/// INSTANTIATE /// +/////////////////// + +// The InstantiateMsg struct defines the initialization parameters for the IBC Transfer Adapter contracts. +#[cw_serde] +pub struct InstantiateMsg { + pub entry_point_contract_address: String, +} + +/////////////// +/// EXECUTE /// +/////////////// + +// The ExecuteMsg enum defines the execution message that the IBC Transfer Adapter contracts can handle. +#[cw_serde] +pub enum ExecuteMsg { + HplTransfer { + dest_domain: u32, + recipient: HexBinary, + hook: Option, + metadata: Option, + warp_address: String, + }, +} diff --git a/packages/secret-skip/src/ibc.rs b/packages/secret-skip/src/ibc.rs new file mode 100644 index 00000000..0828d5f6 --- /dev/null +++ b/packages/secret-skip/src/ibc.rs @@ -0,0 +1,283 @@ +use crate::{error::SkipError, snip20::Snip20ReceiveMsg}; + +use std::convert::From; + +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Coin, ContractInfo, StdError}; + +// Used with Secret network ICS20 contract +// for auto wrapping & unrapping of snip20 tokens +// for IBC transfers +#[cw_serde] +pub struct Ics20TransferMsg { + /// The local channel to send the packets on + pub channel: String, + /// The remote address to send to + /// Don't use HumanAddress as this will likely have a different Bech32 prefix than we use + /// and cannot be validated locally + pub remote_address: String, + /// How long the packet lives in seconds. If not specified, use default_timeout + pub timeout: Option, +} + +/////////////// +/// MIGRATE /// +/////////////// + +// The MigrateMsg struct defines the migration parameters used. +#[cw_serde] +pub struct MigrateMsg { + pub entry_point_contract: ContractInfo, + pub ics20_contract: ContractInfo, +} +/////////////////// +/// INSTANTIATE /// +/////////////////// + +// The InstantiateMsg struct defines the initialization parameters for the IBC Transfer Adapter contracts. +#[cw_serde] +pub struct InstantiateMsg { + pub entry_point_contract: ContractInfo, + pub ics20_contract: ContractInfo, +} + +/////////////// +/// EXECUTE /// +/////////////// + +// The ExecuteMsg enum defines the execution message that the IBC Transfer Adapter contracts can handle. +#[cw_serde] +pub enum ExecuteMsg { + IbcTransfer { + info: IbcInfo, + coin: Coin, + timeout_timestamp: u64, + }, + RegisterTokens { + contracts: Vec, + }, + Receive(Snip20ReceiveMsg), +} + +#[cw_serde] +pub enum Snip20HookMsg { + IbcTransfer { + info: IbcInfo, + timeout_timestamp: u64, + }, +} + +///////////// +/// QUERY /// +///////////// + +// The QueryMsg enum defines the queries the IBC Transfer Adapter Contract provides. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(String)] + InProgressRecoverAddress { + channel_id: String, + sequence_id: u64, + }, +} + +//////////////////// +/// COMMON TYPES /// +//////////////////// + +// The IbcFee struct defines the fees for an IBC transfer standardized across all IBC Transfer Adapter contracts. +#[cw_serde] +#[derive(Default)] +pub struct IbcFee { + pub recv_fee: Vec, + pub ack_fee: Vec, + pub timeout_fee: Vec, +} + +// Converts an IbcFee struct to a cosmwasm_std::Coins struct +// Must be TryFrom since adding the ibc_fees can overflow. +impl TryFrom for Vec { + type Error = StdError; + + fn try_from(ibc_fee: IbcFee) -> Result { + Ok([ibc_fee.recv_fee, ibc_fee.ack_fee, ibc_fee.timeout_fee] + .into_iter() + .flatten() + .collect()) + } +} + +impl IbcFee { + // one_coin aims to mimic the behavior of cw_utls::one_coin, + // returing the single coin in the IbcFee struct if it exists, + // erroring if 0 or more than 1 coins exist. + // + // one_coin is used because the entry_point contract only supports + // the handling of a single denomination for IBC fees. + pub fn one_coin(&self) -> Result { + let ibc_fees: Vec = self.clone().try_into()?; + + if ibc_fees.len() != 1 { + return Err(SkipError::IbcFeesNotOneCoin); + } + + Ok(ibc_fees.first().unwrap().clone()) + } +} + +// The IbcInfo struct defines the information for an IBC transfer standardized across all IBC Transfer Adapter contracts. +#[cw_serde] +pub struct IbcInfo { + pub source_channel: String, + pub receiver: String, + pub fee: Option, + pub memo: String, + pub recover_address: String, +} + +// The IbcTransfer struct defines the parameters for an IBC transfer standardized across all IBC Transfer Adapter contracts. +#[cw_serde] +pub struct IbcTransfer { + pub info: IbcInfo, + pub coin: Coin, + pub timeout_timestamp: u64, +} + +// Converts an IbcTransfer struct to an ExecuteMsg::IbcTransfer enum +impl From for ExecuteMsg { + fn from(ibc_transfer: IbcTransfer) -> Self { + ExecuteMsg::IbcTransfer { + info: ibc_transfer.info, + coin: ibc_transfer.coin, + timeout_timestamp: ibc_transfer.timeout_timestamp, + } + } +} + +// AckID is a type alias for a tuple of a str and a u64 +// which is used as a lookup key to store the in progress +// ibc transfer upon receiving a successful sub msg reply. +pub type AckID<'a> = (&'a str, u64); + +// The IbcLifecycleComplete enum defines the possible sudo messages that the +// ibc transfer adapter contract on ibc-hook enabled chains can expect to received +// from the ibc-hooks module. +#[cw_serde] +pub enum IbcLifecycleComplete { + IbcAck { + /// The source channel of the IBC packet + channel: String, + /// The sequence number that the packet was sent with + sequence: u64, + /// String encoded version of the ack as seen by OnAcknowledgementPacket(..) + ack: String, + /// Whether an ack is a success of failure according to the transfer spec + success: bool, + }, + IbcTimeout { + /// The source channel of the IBC packet + channel: String, + /// The sequence number that the packet was sent with + sequence: u64, + }, +} + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::Uint128; + + #[test] + fn test_from_ibc_fee_for_neutron_proto_fee() { + let ibc_fee = IbcFee { + recv_fee: vec![Coin::new(100, "atom")], + ack_fee: vec![Coin::new(100, "osmo")], + timeout_fee: vec![Coin::new(100, "ntrn")], + }; + + let neutron_fee: NeutronFee = ibc_fee.into(); + + assert_eq!(neutron_fee.recv_fee.len(), 1); + assert_eq!(neutron_fee.ack_fee.len(), 1); + assert_eq!(neutron_fee.timeout_fee.len(), 1); + + assert_eq!(neutron_fee.recv_fee[0].denom, "atom"); + assert_eq!(neutron_fee.recv_fee[0].amount, "100"); + + assert_eq!(neutron_fee.ack_fee[0].denom, "osmo"); + assert_eq!(neutron_fee.ack_fee[0].amount, "100"); + + assert_eq!(neutron_fee.timeout_fee[0].denom, "ntrn"); + assert_eq!(neutron_fee.timeout_fee[0].amount, "100"); + } + + /* + #[test] + fn test_try_from_ibc_fee_for_coins() { + // TEST CASE 1: Same Denom For All Fees + let ibc_fee = IbcFee { + recv_fee: vec![Coin::new(100, "atom")], + ack_fee: vec![Coin::new(100, "atom")], + timeout_fee: vec![Coin::new(100, "atom")], + }; + + let coins: Coins = ibc_fee.try_into().unwrap(); + + assert_eq!(coins.len(), 1); + assert_eq!(coins.amount_of("atom"), Uint128::from(300u128)); + + // TEST CASE 2: Different Denom For Some Fees + let ibc_fee = IbcFee { + recv_fee: vec![Coin::new(100, "atom")], + ack_fee: vec![Coin::new(100, "osmo")], + timeout_fee: vec![Coin::new(100, "atom")], + }; + + let coins: Coins = ibc_fee.try_into().unwrap(); + + assert_eq!(coins.len(), 2); + assert_eq!(coins.amount_of("atom"), Uint128::from(200u128)); + assert_eq!(coins.amount_of("osmo"), Uint128::from(100u128)); + } + */ + + #[test] + fn test_one_coin() { + // TEST CASE 1: No Coins + let ibc_fee = IbcFee { + recv_fee: vec![], + ack_fee: vec![], + timeout_fee: vec![], + }; + + let result = ibc_fee.one_coin(); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), SkipError::IbcFeesNotOneCoin); + + // TEST CASE 2: One Coin + let ibc_fee = IbcFee { + recv_fee: vec![Coin::new(100, "atom")], + ack_fee: vec![], + timeout_fee: vec![], + }; + + let result = ibc_fee.one_coin(); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), Coin::new(100, "atom")); + + // TEST CASE 3: More Than One Coin + let ibc_fee = IbcFee { + recv_fee: vec![Coin::new(100, "atom")], + ack_fee: vec![Coin::new(100, "osmo")], + timeout_fee: vec![], + }; + + let result = ibc_fee.one_coin(); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), SkipError::IbcFeesNotOneCoin); + } +} diff --git a/packages/secret-skip/src/lib.rs b/packages/secret-skip/src/lib.rs new file mode 100644 index 00000000..0a30d471 --- /dev/null +++ b/packages/secret-skip/src/lib.rs @@ -0,0 +1,10 @@ +pub mod asset; +pub mod cw20; +pub mod entry_point; +pub mod error; +pub mod hyperlane; +pub mod ibc; +pub mod proto_coin; +pub mod snip20; +pub mod sudo; +pub mod swap; diff --git a/packages/secret-skip/src/proto_coin.rs b/packages/secret-skip/src/proto_coin.rs new file mode 100644 index 00000000..717d900c --- /dev/null +++ b/packages/secret-skip/src/proto_coin.rs @@ -0,0 +1,92 @@ +use cosmos_sdk_proto::cosmos::base::v1beta1::Coin as CosmosSdkCoin; +use cosmwasm_schema::cw_serde; +use ibc_proto::cosmos::base::v1beta1::Coin as IbcCoin; +// use osmosis_std::types::cosmos::base::v1beta1::Coin as OsmosisStdCoin; + +// Skip wrapper coin type that is used to wrap cosmwasm_std::Coin +// and be able to implement type conversions on the wrapped type. +#[cw_serde] +pub struct ProtoCoin(pub cosmwasm_std::Coin); + +// Converts a skip coin to a cosmos_sdk_proto coin +impl From for CosmosSdkCoin { + fn from(coin: ProtoCoin) -> Self { + // Convert the skip coin to a cosmos_sdk_proto coin and return it + CosmosSdkCoin { + denom: coin.0.denom.clone(), + amount: coin.0.amount.to_string(), + } + } +} + +// Converts a skip coin to an ibc_proto coin +impl From for IbcCoin { + fn from(coin: ProtoCoin) -> Self { + // Convert the skip coin to an ibc_proto coin and return it + IbcCoin { + denom: coin.0.denom, + amount: coin.0.amount.to_string(), + } + } +} + +/* +// Converts a skip coin to an osmosis_std coin +impl From for OsmosisStdCoin { + fn from(coin: ProtoCoin) -> Self { + // Convert the skip coin to an osmosis coin and return it + OsmosisStdCoin { + denom: coin.0.denom, + amount: coin.0.amount.to_string(), + } + } +} +*/ + +#[cfg(test)] +mod tests { + use super::*; + + use cosmwasm_std::Uint128; + + #[test] + fn test_from_skip_proto_coin_to_cosmos_sdk_proto_coin() { + let skip_coin = ProtoCoin(cosmwasm_std::Coin { + denom: "uatom".to_string(), + amount: Uint128::new(100), + }); + + let cosmos_sdk_proto_coin: CosmosSdkCoin = skip_coin.into(); + + assert_eq!(cosmos_sdk_proto_coin.denom, "uatom"); + assert_eq!(cosmos_sdk_proto_coin.amount, "100"); + } + + #[test] + fn test_from_skip_proto_coin_to_ibc_proto_coin() { + let skip_coin = ProtoCoin(cosmwasm_std::Coin { + denom: "uatom".to_string(), + amount: Uint128::new(100), + }); + + let ibc_proto_coin: IbcCoin = skip_coin.into(); + + assert_eq!(ibc_proto_coin.denom, "uatom"); + assert_eq!(ibc_proto_coin.amount, "100"); + } + + /* + #[test] + fn test_from_skip_proto_coin_to_osmosis_std_coin() { + let skip_coin = ProtoCoin(cosmwasm_std::Coin { + denom: "uatom".to_string(), + amount: Uint128::new(100), + }); + + let osmosis_std_coin: OsmosisStdCoin = skip_coin.into(); + + assert_eq!(osmosis_std_coin.denom, "uatom"); + assert_eq!(osmosis_std_coin.amount, "100"); + } + */ +} diff --git a/packages/secret-skip/src/snip20.rs b/packages/secret-skip/src/snip20.rs new file mode 100644 index 00000000..36fa246c --- /dev/null +++ b/packages/secret-skip/src/snip20.rs @@ -0,0 +1,39 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Binary, Uint128}; + +#[cw_serde] +pub struct Snip20ReceiveMsg { + pub sender: Addr, + pub from: Addr, + pub amount: Uint128, + pub memo: Option, + pub msg: Option, +} + +#[cw_serde] +pub enum ExecuteMsg { + Transfer { + recipient: String, + amount: Uint128, + memo: Option, + padding: Option, + }, + Send { + recipient: String, + recipient_code_hash: Option, + amount: Uint128, + msg: Option, + memo: Option, + padding: Option, + }, +} + +#[cw_serde] +pub enum QueryResponse { + Balance { amount: Uint128 }, +} + +#[cw_serde] +pub struct BalanceResponse { + pub amount: Uint128, +} diff --git a/packages/secret-skip/src/sudo.rs b/packages/secret-skip/src/sudo.rs new file mode 100644 index 00000000..6d10744d --- /dev/null +++ b/packages/secret-skip/src/sudo.rs @@ -0,0 +1,27 @@ +use crate::ibc::IbcLifecycleComplete; + +use cosmwasm_schema::cw_serde; + +// SudoType used to give info in response attributes when the sudo function is called +pub enum SudoType { + Response, + Error, + Timeout, +} + +// Implement the From trait for SudoType to convert it to a string to be used in response attributes +impl From for String { + fn from(sudo_type: SudoType) -> Self { + match sudo_type { + SudoType::Response => "sudo_ack_success".into(), + SudoType::Error => "sudo_ack_error_and_bank_send".into(), + SudoType::Timeout => "sudo_timeout_and_bank_send".into(), + } + } +} + +// Message type for Osmosis `sudo` entry_point to interact with callbacks from the ibc hooks module +#[cw_serde] +pub enum OsmosisSudoMsg { + IbcLifecycleComplete(IbcLifecycleComplete), +} diff --git a/packages/secret-skip/src/swap.rs b/packages/secret-skip/src/swap.rs new file mode 100644 index 00000000..38ce37e0 --- /dev/null +++ b/packages/secret-skip/src/swap.rs @@ -0,0 +1,411 @@ +use crate::{asset::Asset, error::SkipError, snip20::Snip20ReceiveMsg}; + +use std::{convert::TryFrom, num::ParseIntError}; + +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Binary, ContractInfo, Decimal, Uint128}; + +/////////////// +/// MIGRATE /// +/////////////// + +// The MigrateMsg struct defines the migration parameters used. +#[cw_serde] +pub struct MigrateMsg { + pub entry_point_contract_address: String, +} + +/////////////////// +/// INSTANTIATE /// +/////////////////// + +// The InstantiateMsg struct defines the initialization parameters for the +// Osmosis Poolmanager and Astroport swap adapter contracts. +#[cw_serde] +pub struct InstantiateMsg { + pub entry_point_contract_address: String, +} + +///////////////////////// +/// EXECUTE /// +///////////////////////// + +// The ExecuteMsg enum defines the execution message that the swap adapter contracts can handle. +// Only the Swap message is callable by external users. +#[cw_serde] +pub enum ExecuteMsg { + Receive(Snip20ReceiveMsg), + Swap { operations: Vec }, + TransferFundsBack { swapper: Addr, return_denom: String }, +} + +#[cw_serde] +pub enum Cw20HookMsg { + Swap { operations: Vec }, +} + +///////////////////////// +/// QUERY /// +///////////////////////// + +// The QueryMsg enum defines the queries the swap adapter contracts provide. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + // SimulateSwapExactAssetOut returns the asset in necessary to receive the specified asset out + #[returns(Asset)] + SimulateSwapExactAssetOut { + asset_out: Asset, + swap_operations: Vec, + }, + // SimulateSwapExactAssetIn returns the asset out received from the specified asset in + #[returns(Asset)] + SimulateSwapExactAssetIn { + asset_in: Asset, + swap_operations: Vec, + }, + // SimulateSwapExactAssetOutWithSpotPrice returns the asset in necessary to receive the specified asset out with metadata + #[returns(SimulateSwapExactAssetOutResponse)] + SimulateSwapExactAssetOutWithMetadata { + asset_out: Asset, + swap_operations: Vec, + include_spot_price: bool, + }, + // SimulateSwapExactAssetInWithSpotPrice returns the asset out received from the specified asset in with metadata + #[returns(SimulateSwapExactAssetInResponse)] + SimulateSwapExactAssetInWithMetadata { + asset_in: Asset, + swap_operations: Vec, + include_spot_price: bool, + }, + // SimulateSmartSwapExactAssetIn returns the asset out received from the specified asset in over multiple routes + #[returns(Asset)] + SimulateSmartSwapExactAssetIn { asset_in: Asset, routes: Vec }, + // SimulateSmartSwapExactAssetInWithMetadata returns the asset out received from the specified asset in over multiple routes with metadata + #[returns(SimulateSmartSwapExactAssetInResponse)] + SimulateSmartSwapExactAssetInWithMetadata { + asset_in: Asset, + routes: Vec, + include_spot_price: bool, + }, +} + +// The SimulateSwapExactAssetInResponse struct defines the response for the +// SimulateSwapExactAssetIn query. +#[cw_serde] +pub struct SimulateSwapExactAssetInResponse { + pub asset_out: Asset, + pub spot_price: Option, +} + +// The SimulateSwapExactAssetOutResponse struct defines the response for the +// SimulateSwapExactAssetOut query. +#[cw_serde] +pub struct SimulateSwapExactAssetOutResponse { + pub asset_in: Asset, + pub spot_price: Option, +} + +// The SimulateSmartSwapExactAssetInResponse struct defines the response for the +// SimulateSmartSwapExactAssetIn query. +#[cw_serde] +pub struct SimulateSmartSwapExactAssetInResponse { + pub asset_out: Asset, + pub spot_price: Option, +} + +//////////////////// +/// COMMON TYPES /// +//////////////////// + +// Swap venue object that contains the name of the swap venue and adapter contract address. +#[cw_serde] +pub struct SwapVenue { + pub name: String, + pub adapter_contract: ContractInfo, +} + +#[cw_serde] +pub struct Route { + pub offer_asset: Asset, + pub operations: Vec, +} + +impl Route { + pub fn ask_denom(&self) -> Result { + match self.operations.last() { + Some(op) => Ok(op.denom_out.clone()), + None => Err(SkipError::SwapOperationsEmpty), + } + } +} + +pub fn get_ask_denom_for_routes(routes: &[Route]) -> Result { + match routes.last() { + Some(route) => route.ask_denom(), + None => Err(SkipError::RoutesEmpty), + } +} + +// Standard swap operation type that contains the pool, denom in, and denom out +// for the swap operation. The type is converted into the respective swap venues +// expected format in each adapter contract. +#[cw_serde] +pub struct SwapOperation { + pub pool: String, + pub denom_in: String, + pub denom_out: String, + pub interface: Option, +} + +// Converts a vector of skip swap operation to vector of osmosis swap +// amount in/out routes, returning an error if any of the swap operations +// fail to convert. This only happens if the given String for pool in the +// swap operation is not a valid u64, which is the pool_id type for Osmosis. +pub fn convert_swap_operations( + swap_operations: Vec, +) -> Result, ParseIntError> +where + T: TryFrom, +{ + swap_operations.into_iter().map(T::try_from).collect() +} + +// Swap object to get the exact amount of a given asset with the given vector of swap operations +#[cw_serde] +pub struct SwapExactAssetOut { + pub swap_venue_name: String, + pub operations: Vec, + pub refund_address: Option, +} + +// Swap object that swaps the remaining asset recevied +// from the contract call minus fee swap (if present) +#[cw_serde] +pub struct SwapExactAssetIn { + pub swap_venue_name: String, + pub operations: Vec, +} + +// Swap object that swaps the remaining asset recevied +// over multiple routes from the contract call minus fee swap (if present) +#[cw_serde] +pub struct SmartSwapExactAssetIn { + pub swap_venue_name: String, + pub routes: Vec, +} + +impl SmartSwapExactAssetIn { + pub fn amount(&self) -> Uint128 { + self.routes + .iter() + .map(|route| route.offer_asset.amount()) + .sum() + } + + pub fn ask_denom(&self) -> Result { + match self.routes.last() { + Some(route) => route.ask_denom(), + None => Err(SkipError::RoutesEmpty), + } + } + + pub fn largest_route_index(&self) -> Result { + match self + .routes + .iter() + .enumerate() + .max_by_key(|(_, route)| route.offer_asset.amount()) + .map(|(index, _)| index) + { + Some(idx) => Ok(idx), + None => Err(SkipError::RoutesEmpty), + } + } +} + +// Converts a SwapExactAssetOut used in the entry point contract +// to a swap adapter Swap execute message +impl From for ExecuteMsg { + fn from(swap: SwapExactAssetOut) -> Self { + ExecuteMsg::Swap { + operations: swap.operations, + } + } +} + +// Converts a SwapExactAssetIn used in the entry point contract +// to a swap adapter Swap execute message +impl From for ExecuteMsg { + fn from(swap: SwapExactAssetIn) -> Self { + ExecuteMsg::Swap { + operations: swap.operations, + } + } +} + +#[cw_serde] +pub enum Swap { + SwapExactAssetIn(SwapExactAssetIn), + SwapExactAssetOut(SwapExactAssetOut), + SmartSwapExactAssetIn(SmartSwapExactAssetIn), +} + +//////////////////////// +/// COMMON FUNCTIONS /// +//////////////////////// + +// Query the contract's balance and transfer the funds back to the swapper +/* +pub fn execute_transfer_funds_back( + deps: DepsMut, + env: Env, + info: MessageInfo, + swapper: Addr, + return_denom: String, +) -> Result { + // Ensure the caller is the contract itself + if info.sender != env.contract.address { + return Err(SkipError::Unauthorized); + } + // Create the transfer funds back message + let transfer_funds_back_msg: CosmosMsg = match deps.api.addr_validate(&return_denom) { + Ok(contract_addr) => Asset::new( + deps.api, + contract_addr.as_str(), + Cw20Contract(contract_addr.clone()).balance(&deps.querier, &env.contract.address)?, + ) + .transfer(swapper.as_str()), + Err(_) => CosmosMsg::Bank(BankMsg::Send { + to_address: swapper.to_string(), + amount: deps.querier.query_all_balances(env.contract.address)?, + }), + }; + + Ok(Response::new() + .add_message(transfer_funds_back_msg) + .add_attribute("action", "dispatch_transfer_funds_back_bank_send")) +} +*/ + +// Validates the swap operations +pub fn validate_swap_operations( + swap_operations: &[SwapOperation], + asset_in_denom: &str, + asset_out_denom: &str, +) -> Result<(), SkipError> { + // Verify the swap operations are not empty + let (Some(first_op), Some(last_op)) = (swap_operations.first(), swap_operations.last()) else { + return Err(SkipError::SwapOperationsEmpty); + }; + + // Verify the first swap operation denom in is the same as the asset in denom + if first_op.denom_in != asset_in_denom { + return Err(SkipError::SwapOperationsAssetInDenomMismatch); + } + + // Verify the last swap operation denom out is the same as the asset out denom + if last_op.denom_out != asset_out_denom { + return Err(SkipError::SwapOperationsAssetOutDenomMismatch); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + use cosmwasm_std::testing::mock_dependencies; + + #[test] + fn test_validate_swap_operations() { + // TEST CASE 1: Valid Swap Operations + let swap_operations = vec![ + SwapOperation { + pool: "1".to_string(), + denom_in: "uatom".to_string(), + denom_out: "uosmo".to_string(), + interface: None, + }, + SwapOperation { + pool: "2".to_string(), + denom_in: "uosmo".to_string(), + denom_out: "untrn".to_string(), + interface: None, + }, + ]; + + let asset_in_denom = "uatom"; + let asset_out_denom = "untrn"; + + let result = validate_swap_operations(&swap_operations, asset_in_denom, asset_out_denom); + + assert!(result.is_ok()); + + // TEST CASE 2: Empty Swap Operations + let swap_operations: Vec = vec![]; + + let asset_in_denom = "uatom"; + let asset_out_denom = "untrn"; + + let result = validate_swap_operations(&swap_operations, asset_in_denom, asset_out_denom); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), SkipError::SwapOperationsEmpty); + + // TEST CASE 3: First Swap Operation Denom In Mismatch + let swap_operations = vec![ + SwapOperation { + pool: "1".to_string(), + denom_in: "uosmo".to_string(), + denom_out: "uatom".to_string(), + interface: None, + }, + SwapOperation { + pool: "2".to_string(), + denom_in: "uatom".to_string(), + denom_out: "untrn".to_string(), + interface: None, + }, + ]; + + let asset_in_denom = "uatom"; + let asset_out_denom = "untrn"; + + let result = validate_swap_operations(&swap_operations, asset_in_denom, asset_out_denom); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + SkipError::SwapOperationsAssetInDenomMismatch + ); + + // TEST CASE 4: Last Swap Operation Denom Out Mismatch + let swap_operations = vec![ + SwapOperation { + pool: "1".to_string(), + denom_in: "uatom".to_string(), + denom_out: "uosmo".to_string(), + interface: None, + }, + SwapOperation { + pool: "2".to_string(), + denom_in: "uosmo".to_string(), + denom_out: "uatom".to_string(), + interface: None, + }, + ]; + + let asset_in_denom = "uatom"; + let asset_out_denom = "untrn"; + + let result = validate_swap_operations(&swap_operations, asset_in_denom, asset_out_denom); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + SkipError::SwapOperationsAssetOutDenomMismatch + ); + } +}