diff --git a/Cargo.lock b/Cargo.lock index 72335784..8b18b4b4 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.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[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 = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "base16ct" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +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.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd50718a2b6830ce9eb5d465de5a018a12e71729d66b70807ce97e6dd14f931d" +checksum = "58535cbcd599b3c193e3967c8292fe1dbbb5de7c2a2d87380661091dd4744044" 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.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "242e98e7a231c122e08f300d9db3262d1007b51758a8732cd6210b3e9faa4f3a" +checksum = "a8e07de16c800ac82fd188d055ecdb923ead0cf33960d3350089260bb982c09f" dependencies = [ "syn 1.0.109", ] [[package]] name = "cosmwasm-schema" -version = "1.5.5" +version = "1.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7879036156092ad1c22fe0d7316efc5a5eceec2bc3906462a2560215f2a2f929" +checksum = "93d388adfa9cb449557a92e9318121ac1a481fc4f599213b03a5b62699b403b4" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -267,9 +288,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.5.5" +version = "1.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb57855fbfc83327f8445ae0d413b1a05ac0d68c396ab4d122b2abd7bb82cb6" +checksum = "2411b389e56e6484f81ba955b758d02522d620c98fc960c4bd2251d48b7aa19f" dependencies = [ "proc-macro2", "quote", @@ -278,11 +299,11 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.5.5" +version = "1.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c1556156fdf892a55cced6115968b961eaaadd6f724a2c2cb7d1e168e32dd3" +checksum = "c21fde95ccd20044a23c0ac6fd8c941f3e8c158169dc94b5aa6491a2d9551a8d" 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#d07e55e17c791a5f6557f114e3fb6cb433d9b800" 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.168" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" [[package]] name = "lido-satellite" @@ -1261,7 +1384,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.90", ] [[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.90", ] [[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]] @@ -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", ] @@ -1497,7 +1630,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.90", ] [[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.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 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.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "ryu" @@ -1653,7 +1797,21 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.72", + "syn 2.0.90", +] + +[[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,14 +1820,134 @@ 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-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" @@ -1678,9 +1956,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.204" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] @@ -1694,6 +1972,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 +2010,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.90", ] [[package]] @@ -1740,14 +2027,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.90", ] [[package]] name = "serde_json" -version = "1.0.122" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -1789,6 +2076,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 +2188,23 @@ dependencies = [ "cw2 1.1.2", ] +[[package]] +name = "skip-go-secret-entry-point" +version = "0.3.0" +dependencies = [ + "cosmwasm-schema", + "cw-utils 1.0.3", + "cw20 1.1.2", + "schemars", + "secret-cosmwasm-std", + "secret-storage-plus", + "secret-toolkit", + "serde", + "skip", + "test-case", + "thiserror", +] + [[package]] name = "skip-go-swap-adapter-astroport" version = "0.3.0" @@ -2040,6 +2354,24 @@ dependencies = [ "thiserror", ] +[[package]] +name = "skip-go-swap-adapter-shade-protocol" +version = "0.3.0" +dependencies = [ + "cosmwasm-schema", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "schemars", + "secret-cosmwasm-std", + "secret-storage-plus", + "secret-toolkit", + "serde", + "skip", + "test-case", + "thiserror", +] + [[package]] name = "skip-go-swap-adapter-white-whale" version = "0.3.0" @@ -2088,6 +2420,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 +2437,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.9", ] [[package]] @@ -2123,7 +2465,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.72", + "syn 2.0.90", ] [[package]] @@ -2154,9 +2496,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.72" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -2223,7 +2565,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.90", ] [[package]] @@ -2234,35 +2576,35 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.90", "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.90", ] [[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 +2621,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 +2649,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 +2684,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/swap/shade-protocol/Cargo.toml b/contracts/adapters/swap/shade-protocol/Cargo.toml new file mode 100644 index 00000000..49d91601 --- /dev/null +++ b/contracts/adapters/swap/shade-protocol/Cargo.toml @@ -0,0 +1,43 @@ +[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] +#astroport = { workspace = true } +#cosmwasm-schema = { 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 } + +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..60c32bc7 --- /dev/null +++ b/contracts/adapters/swap/shade-protocol/src/contract.rs @@ -0,0 +1,425 @@ +use crate::{ + error::{ContractError, ContractResult}, + // skip_error::ContractError, + state::{REGISTERED_TOKENS, STATE}, +}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{ + entry_point, from_binary, to_binary, Addr, Binary, ContractInfo, Deps, DepsMut, Env, + MessageInfo, Response, Uint128, WasmMsg, +}; +use secret_skip::{asset::Asset, swap::SwapOperation}; +// use cw2::set_contract_version; +use cw20::Cw20Coin; +use secret_toolkit::snip20; + +use crate::{ + msg::{Cw20HookMsg, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, Snip20ReceiveMsg}, + shade_swap_router_msg as shade_router, +}; + +#[cw_serde] +pub struct State { + pub entry_point_contract: ContractInfo, + pub shade_router_contract: ContractInfo, + pub shade_pool_code_hash: String, + pub viewing_key: String, +} + +// 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: State) -> 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 + STATE.save( + deps.storage, + &State { + entry_point_contract: checked_entry_point_contract.clone(), + shade_router_contract: msg.shade_router_contract.clone(), + shade_pool_code_hash: msg.shade_pool_code_hash.clone(), + viewing_key: msg.viewing_key.clone(), + }, + )?; + + Ok(Response::new() + .add_attribute("action", "migrate") + .add_attribute( + "entry_point_contract_address", + checked_entry_point_contract.address.to_string(), + )) +} + +/////////////////// +/// INSTANTIATE /// +/////////////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: State, +) -> 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, + }; + + // Store the entry point contract address + STATE.save( + deps.storage, + &State { + entry_point_contract: checked_entry_point_contract.clone(), + shade_router_contract: msg.shade_router_contract.clone(), + shade_pool_code_hash: msg.shade_pool_code_hash.clone(), + viewing_key: msg.viewing_key.clone(), + }, + )?; + + 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)? { + Cw20HookMsg::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), + _ => unimplemented!(), + } +} + +fn execute_swap( + deps: DepsMut, + env: Env, + info: MessageInfo, + operations: Vec, + input_amount: Uint128, +) -> ContractResult { + // Get contract state from storage + let state = STATE.load(deps.storage)?; + + // Enforce the caller is the entry point contract + if info.sender != state.entry_point_contract.address { + return Err(ContractError::Unauthorized); + } + + // 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: state.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), + }; + + // 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( + state.shade_router_contract.address.to_string(), + input_amount, + Some(to_binary(&shade_router::InvokeMsg::SwapTokensForExact { + path, + expected_return: None, + recipient: None, + })?), + None, + None, + 255, + state.shade_router_contract.code_hash, + input_denom, + )?) + // 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: state.entry_point_contract.address, + return_denom, + })?, + funds: vec![], + })) +} + +fn register_tokens( + deps: DepsMut, + env: Env, + contracts: Vec, +) -> ContractResult { + let mut response = Response::new(); + + let state = STATE.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( + state.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) +} + +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); + } + + // load state from storage + let state = match STATE.load(deps.storage) { + Ok(state) => state, + Err(e) => return Err(ContractError::Std(e)), + }; + + // 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 balance = match snip20::balance_query( + deps.querier, + env.contract.address.to_string(), + state.viewing_key, + 255, + token_contract.code_hash.clone(), + token_contract.address.to_string(), + ) { + Ok(balance) => balance, + Err(e) => return Err(ContractError::Std(e)), + }; + + let transfer_msg = match snip20::send_msg( + state.entry_point_contract.address.to_string(), + balance.amount, + None, + None, + None, + 255, + 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(transfer_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, + )?), + _ => unimplemented!(), + } + .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 state = STATE.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: state.shade_pool_code_hash.clone(), + }); + } + + let sim_response: shade_router::QueryMsgResponse = deps.querier.query_wasm_smart( + &state.shade_router_contract.address, + &state.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..4c433822 --- /dev/null +++ b/contracts/adapters/swap/shade-protocol/src/error.rs @@ -0,0 +1,42 @@ +use cosmwasm_std::{OverflowError, StdError}; +use cw_utils; +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..4b7fb5f8 --- /dev/null +++ b/contracts/adapters/swap/shade-protocol/src/msg.rs @@ -0,0 +1,61 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Binary, ContractInfo, Uint128}; +use secret_skip::{asset::Asset, 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 Cw20HookMsg { + Swap { operations: Vec }, +} + +/* +#[cw_serde] +pub struct SwapOperation { + pub pool: String, + pub denom_in: String, + pub denom_out: String, + pub interface: Option, +} +*/ + +#[cw_serde] +pub struct Snip20ReceiveMsg { + pub sender: Addr, + pub from: Addr, + pub amount: Uint128, + pub memo: Option, + pub msg: Option, +} 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..5e9a8d15 --- /dev/null +++ b/contracts/adapters/swap/shade-protocol/src/shade_swap_router_msg.rs @@ -0,0 +1,111 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{ + Addr, Binary, ContractInfo, Decimal, Decimal256, Deps, DepsMut, Env, MessageInfo, Response, + Uint128, Uint256, WasmMsg, +}; +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); + +pub struct TokenPairIterator<'a> { + pair: &'a TokenPair, + index: u8, +} + +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..2addcc98 --- /dev/null +++ b/contracts/adapters/swap/shade-protocol/src/state.rs @@ -0,0 +1,8 @@ +use crate::contract::State; +use cosmwasm_std::{Addr, ContractInfo}; +use secret_storage_plus::Map; +use secret_toolkit::storage::Item; + +pub const STATE: Item = Item::new(b"state"); + +pub const REGISTERED_TOKENS: Map = Map::new("registered_tokens"); diff --git a/contracts/secret-entry-point/Cargo.toml b/contracts/secret-entry-point/Cargo.toml new file mode 100644 index 00000000..f7f29c70 --- /dev/null +++ b/contracts/secret-entry-point/Cargo.toml @@ -0,0 +1,40 @@ +[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 } 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..1046f8be --- /dev/null +++ b/contracts/secret-entry-point/src/bin/schema.rs @@ -0,0 +1,10 @@ +use cosmwasm_schema::write_api; +use skip::entry_point::{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..bc51c606 --- /dev/null +++ b/contracts/secret-entry-point/src/contract.rs @@ -0,0 +1,312 @@ +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, HYPERLANE_TRANSFER_CONTRACT_ADDRESS, + IBC_TRANSFER_CONTRACT_ADDRESS, 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; +// use cw2::set_contract_version; + +/////////////// +/// 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_ADDRESS.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..375cbc90 --- /dev/null +++ b/contracts/secret-entry-point/src/error.rs @@ -0,0 +1,102 @@ +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), +} diff --git a/contracts/secret-entry-point/src/execute.rs b/contracts/secret-entry-point/src/execute.rs new file mode 100644 index 00000000..0a4f113a --- /dev/null +++ b/contracts/secret-entry-point/src/execute.rs @@ -0,0 +1,1111 @@ +use std::vec; + +use crate::{ + error::{ContractError, ContractResult}, + hyperlane::{ExecuteMsg as HplExecuteMsg, ExecuteMsg::HplTransfer}, + msg::{Action, Affiliate, ExecuteMsg, Snip20HookMsg, Snip20ReceiveMsg}, + reply::{RecoverTempStorage, RECOVER_REPLY_ID}, + state::{ + BLOCKED_CONTRACT_ADDRESSES, HYPERLANE_TRANSFER_CONTRACT_ADDRESS, + IBC_TRANSFER_CONTRACT_ADDRESS, PRE_SWAP_OUT_ASSET_AMOUNT, RECOVER_TEMP_STORAGE, + REGISTERED_TOKENS, SWAP_VENUE_MAP, VIEWING_KEY, + }, +}; + +use secret_skip::asset::Asset; + +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 cw20::Cw20Coin; +use secret_skip::{ + error::SkipError, + ibc::{ExecuteMsg as IbcTransferExecuteMsg, IbcInfo, IbcTransfer}, + swap::{validate_swap_operations, Swap, SwapExactAssetOut}, +}; +use skip_go_swap_adapter_shade_protocol::msg::{ + Cw20HookMsg as SwapHookMsg, ExecuteMsg as SwapExecuteMsg, QueryMsg as SwapQueryMsg, +}; + +////////////////////////// +/// 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, + 255, + 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, + 255, + 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, + 255, + 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, + 255, + 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, + 255, + 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, + 255, + 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, + 255, + 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)?; + + // Create the transfer message + // let transfer_msg = action_asset.transfer(&to_address); + let action_asset_contract = REGISTERED_TOKENS + .load(deps.storage, deps.api.addr_validate(action_asset.denom())?)?; + let transfer_msg = match snip20::transfer_msg( + to_address.to_string(), + action_asset.amount(), + None, + None, + 255, + 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_coin = match action_asset { + Asset::Native(coin) => { + return Err(ContractError::NativeCoinNotSupported); + } + _ => return Err(ContractError::NonNativeIbcTransfer), + }; + todo!("Implement IBC Transfer for Snip20"); + + // Create the IBC transfer message + let ibc_transfer_msg: IbcTransferExecuteMsg = IbcTransfer { + info: ibc_info, + coin: transfer_out_coin.clone(), + timeout_timestamp, + } + .into(); + + // Get the IBC transfer adapter contract address + let ibc_transfer_contract = IBC_TRANSFER_CONTRACT_ADDRESS.load(deps.storage)?; + + // Send the IBC transfer by calling the IBC transfer contract + let ibc_transfer_msg = WasmMsg::Execute { + contract_addr: ibc_transfer_contract.address.to_string(), + code_hash: ibc_transfer_contract.code_hash.clone(), + msg: to_binary(&ibc_transfer_msg)?, + funds: vec![transfer_out_coin], + }; + + // 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); + } + + // Create the contract call message + let contract_call_msg = action_asset.into_wasm_msg(contract_address, msg)?; + + // 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"); + } + }; + + 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 { + 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_ADDRESS.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, + 255, + 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..bddb5f9c --- /dev/null +++ b/contracts/secret-entry-point/src/msg.rs @@ -0,0 +1,190 @@ +use secret_skip::{ + asset::Asset, + ibc::IbcInfo, + 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, +} + +#[cw_serde] +pub struct Snip20ReceiveMsg { + pub sender: Addr, + pub from: Addr, + pub amount: Uint128, + pub memo: Option, + pub msg: Option, +} + +/////////////// +/// 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..ba521df9 --- /dev/null +++ b/contracts/secret-entry-point/src/query.rs @@ -0,0 +1,12 @@ +use crate::state::{IBC_TRANSFER_CONTRACT_ADDRESS, 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_ADDRESS.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..cda4ea26 --- /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, + 255, + 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..91dcbfae --- /dev/null +++ b/contracts/secret-entry-point/src/state.rs @@ -0,0 +1,25 @@ +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_ADDRESS: Item = + Item::new("ibc_transfer_contract_address"); +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/packages/secret-skip/Cargo.toml b/packages/secret-skip/Cargo.toml new file mode 100644 index 00000000..d3273020 --- /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..38720e99 --- /dev/null +++ b/packages/secret-skip/src/asset.rs @@ -0,0 +1,659 @@ +use crate::error::SkipError; +// use astroport::asset::{Asset as AstroportAsset, AssetInfo}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{ + to_binary, Api, BankMsg, Binary, Coin, CosmosMsg, DepsMut, Env, MessageInfo, OverflowError, + OverflowOperation, Uint128, WasmMsg, +}; +use cw20::{Cw20Coin, Cw20CoinVerified, Cw20Contract, Cw20ExecuteMsg}; +use cw_utils::{nonpayable, one_coin}; +/* +use white_whale_std::pool_network::asset::{ + Asset as WhiteWhaleAsset, AssetInfo as WhiteWhaleAssetInfo, +}; +*/ + +#[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; + use cw_utils::PaymentError; + + #[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::Payment(PaymentError::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::Payment(PaymentError::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/entry_point.rs b/packages/secret-skip/src/entry_point.rs new file mode 100644 index 00000000..2217cbe9 --- /dev/null +++ b/packages/secret-skip/src/entry_point.rs @@ -0,0 +1,172 @@ +use crate::{ + asset::Asset, + ibc::IbcInfo, + swap::{Swap, SwapExactAssetOut, SwapVenue}, +}; + +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Binary, HexBinary, Uint128}; +use cw20::Cw20ReceiveMsg; + +/////////////// +/// 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(Cw20ReceiveMsg), + 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..1d327f04 --- /dev/null +++ b/packages/secret-skip/src/error.rs @@ -0,0 +1,54 @@ +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] cw_utils::PaymentError), + + #[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..94eb2582 --- /dev/null +++ b/packages/secret-skip/src/ibc.rs @@ -0,0 +1,288 @@ +use crate::{error::SkipError, proto_coin::ProtoCoin}; + +use std::convert::From; + +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Coin, StdError}; +// use neutron_proto::neutron::feerefunder::Fee as NeutronFee; + +/////////////// +/// 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 { + IbcTransfer { + info: IbcInfo, + coin: Coin, + 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 neutron_proto::neutron::feerefunder Fee +impl From for NeutronFee { + fn from(ibc_fee: IbcFee) -> Self { + NeutronFee { + recv_fee: ibc_fee + .recv_fee + .iter() + .map(|coin| ProtoCoin(coin.clone()).into()) + .collect(), + ack_fee: ibc_fee + .ack_fee + .iter() + .map(|coin| ProtoCoin(coin.clone()).into()) + .collect(), + timeout_fee: ibc_fee + .timeout_fee + .iter() + .map(|coin| ProtoCoin(coin.clone()).into()) + .collect(), + } + } +} +*/ + +/* +// Converts an IbcFee struct to a cosmwasm_std::Coins struct +// Must be TryFrom since adding the ibc_fees can overflow. +impl TryFrom for Coins { + type Error = StdError; + + fn try_from(ibc_fee: IbcFee) -> Result { + let mut ibc_fees = Coins::default(); + + [ibc_fee.recv_fee, ibc_fee.ack_fee, ibc_fee.timeout_fee] + .into_iter() + .flatten() + .try_for_each(|coin| ibc_fees.add(coin))?; + + Ok(ibc_fees) + } +} +*/ + +/* +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_map: Coins = self.clone().try_into()?; + + if ibc_fees_map.len() != 1 { + return Err(SkipError::IbcFeesNotOneCoin); + } + + Ok(ibc_fees_map.to_vec().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..5f1d7195 --- /dev/null +++ b/packages/secret-skip/src/lib.rs @@ -0,0 +1,8 @@ +pub mod asset; +pub mod entry_point; +pub mod error; +pub mod hyperlane; +pub mod ibc; +pub mod proto_coin; +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/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..f7ebf17a --- /dev/null +++ b/packages/secret-skip/src/swap.rs @@ -0,0 +1,422 @@ +use crate::{asset::Asset, error::SkipError}; + +use std::{convert::TryFrom, num::ParseIntError}; + +// use astroport::{asset::AssetInfo, router::SwapOperation as AstroportSwapOperation}; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{ + Addr, BankMsg, Binary, ContractInfo, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Response, + Uint128, +}; +use cw20::Cw20Contract; +use cw20::Cw20ReceiveMsg; +/* +use osmosis_std::types::osmosis::poolmanager::v1beta1::{ + SwapAmountInRoute as OsmosisSwapAmountInRoute, SwapAmountOutRoute as OsmosisSwapAmountOutRoute, +}; +*/ + +/////////////// +/// 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(Cw20ReceiveMsg), + 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 + ); + } +}