diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..dfe07704 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 00000000..628b116e --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,58 @@ +name: Build / Test On PR + +on: [pull_request] + +jobs: + check: + name: Check + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v3 + + - name: Install toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + targets: wasm32-unknown-unknown + + - name: Check for errors + # use wasm32 target so that if we have included wasm-incompatible + # dependencies, it will result in CI failure + run: cargo check --locked --target wasm32-unknown-unknown + env: + RUST_BACKTRACE: 1 + + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v3 + + - name: Install toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + + - name: Run tests + run: cargo test --lib --locked --tests + env: + RUST_BACKTRACE: 1 + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v3 + + - name: Install toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: nightly + components: clippy + + - name: Run clippy + # use nightly toolchain so that we get all the latest linter features + run: cargo +nightly clippy --tests -- -D warnings diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..7cec5f84 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Build results +target +artifacts +/schema + +# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) +.cargo-ok + +# Text file backups +**/*.rs.bk + +# macOS +.DS_Store + +# IDEs +*.iml +.idea + +# Things not ready yet +LICENSE +NOTICE +.cargo +.circleci +.editorconfig + +# Python venv +venv +.venv +.env \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..5ce017cb --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1295 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "anyhow" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + +[[package]] +name = "astroport" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcea351626899d205aab091c891fc878fc9b3c930585fd3ef6222de028d8a7a" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.15.1", + "cw-utils 0.15.1", + "cw20", + "itertools", + "uint", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +dependencies = [ + "serde", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +dependencies = [ + "android-tzdata", + "num-traits", +] + +[[package]] +name = "const-oid" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" + +[[package]] +name = "cosmos-sdk-proto" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4776e787b24d9568dd61d3237eeb4eb321d622fb881b858c7b82806420e87d4" +dependencies = [ + "prost", + "prost-types", + "tendermint-proto 0.27.0", +] + +[[package]] +name = "cosmos-sdk-proto" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73c9d2043a9e617b0d602fbc0a0ecd621568edbf3a9774890a6d562389bd8e1c" +dependencies = [ + "prost", + "prost-types", + "tendermint-proto 0.32.0", +] + +[[package]] +name = "cosmwasm-crypto" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41c0e41be7e6c7d7ab3c61cdc32fcfaa14f948491a401cbc1c74bb33b6f4b851" +dependencies = [ + "digest 0.10.7", + "ed25519-zebra", + "k256", + "rand_core 0.6.4", + "thiserror", +] + +[[package]] +name = "cosmwasm-derive" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a7ee2798c92c00dd17bebb4210f81d5f647e5e92d847959b7977e0fd29a3500" +dependencies = [ + "syn 1.0.109", +] + +[[package]] +name = "cosmwasm-schema" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "407aca6f1671a08b60db8167f03bb7cb6b2378f0ddd9a030367b66ba33c2fd41" +dependencies = [ + "cosmwasm-schema-derive", + "schemars", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cosmwasm-schema-derive" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d1e00b8fd27ff923c10303023626358e23a6f9079f8ebec23a8b4b0bfcd4b3" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cosmwasm-std" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d5fdfd112b070055f068fad079d490117c8e905a588b92a5a7c9276d029930" +dependencies = [ + "base64 0.13.1", + "cosmwasm-crypto", + "cosmwasm-derive", + "derivative", + "forward_ref", + "hex", + "schemars", + "serde", + "serde-json-wasm 0.5.1", + "sha2 0.10.6", + "thiserror", + "uint", +] + +[[package]] +name = "cpufeatures" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-bigint" +version = "0.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-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "cw-storage-plus" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6cf70ef7686e2da9ad7b067c5942cd3e88dd9453f7af42f54557f8af300fb0" +dependencies = [ + "cosmwasm-std", + "schemars", + "serde", +] + +[[package]] +name = "cw-storage-plus" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053a5083c258acd68386734f428a5a171b29f7d733151ae83090c6fcc9417ffa" +dependencies = [ + "cosmwasm-std", + "schemars", + "serde", +] + +[[package]] +name = "cw-utils" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae0b69fa7679de78825b4edeeec045066aa2b2c4b6e063d80042e565bb4da5c" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw2 0.15.1", + "schemars", + "semver", + "serde", + "thiserror", +] + +[[package]] +name = "cw-utils" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c80e93d1deccb8588db03945016a292c3c631e6325d349ebb35d2db6f4f946f7" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw2 1.0.1", + "schemars", + "semver", + "serde", + "thiserror", +] + +[[package]] +name = "cw2" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5abb8ecea72e09afff830252963cb60faf945ce6cef2c20a43814516082653da" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.15.1", + "schemars", + "serde", +] + +[[package]] +name = "cw2" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb70cee2cf0b4a8ff7253e6bc6647107905e8eb37208f87d54f67810faa62f8" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.0.1", + "schemars", + "serde", +] + +[[package]] +name = "cw20" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6025276fb6e603e974c21f3e4606982cdc646080e8fba3198816605505e1d9a" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils 0.15.1", + "schemars", + "serde", +] + +[[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 = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", + "subtle", +] + +[[package]] +name = "dyn-clone" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" + +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "ed25519-zebra" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" +dependencies = [ + "curve25519-dalek", + "hashbrown", + "hex", + "rand_core 0.6.4", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct", + "crypto-bigint", + "der", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "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 = "flex-error" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c606d892c9de11507fa0dcffc116434f94e105d0bbdc4e405b61519464c49d7b" +dependencies = [ + "paste", +] + +[[package]] +name = "forward_ref" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "ibc-proto" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c888103095b45bee90cb9104513ade30abd69902153b0682b5ad81940ae1f865" +dependencies = [ + "base64 0.21.2", + "bytes", + "flex-error", + "ics23", + "prost", + "serde", + "subtle-encoding", + "tendermint-proto 0.32.0", +] + +[[package]] +name = "ics23" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9e8f569c5cc88e08b8d076dc207e0748aa1f52d4b84910ec919c8f2bed6ea7" +dependencies = [ + "anyhow", + "bytes", + "hex", + "prost", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "k256" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "sha2 0.10.6", +] + +[[package]] +name = "libc" +version = "0.2.146" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" + +[[package]] +name = "neutron-proto" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943ccd9f9749e96095237502e1c61abc867fd5cf28668097b359b0d61aa50c96" +dependencies = [ + "cosmos-sdk-proto 0.19.0", + "cosmwasm-std", + "prost", + "prost-types", + "tendermint-proto 0.32.0", +] + +[[package]] +name = "neutron-sdk" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487305afc06808aabe5b90684bc238c93fb8872a275edf6db4ea24b102eb9a8e" +dependencies = [ + "base64 0.20.0", + "bech32", + "cosmos-sdk-proto 0.16.0", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.0.1", + "prost", + "protobuf", + "schemars", + "serde", + "serde-json-wasm 0.4.1", + "serde_json", + "thiserror", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "osmosis-std" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87725a7480b98887167edf878daa52201a13322ad88e34355a7f2ddc663e047e" +dependencies = [ + "chrono", + "cosmwasm-std", + "osmosis-std-derive", + "prost", + "prost-types", + "schemars", + "serde", + "serde-cw-value", +] + +[[package]] +name = "osmosis-std-derive" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4d482a16be198ee04e0f94e10dd9b8d02332dcf33bc5ea4b255e7e25eedc5df" +dependencies = [ + "itertools", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "prost-types" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" +dependencies = [ + "prost", +] + +[[package]] +name = "protobuf" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55bad9126f378a853655831eb7363b7b01b81d19f8cb1218861086ca4a1a61e" +dependencies = [ + "bytes", + "once_cell", + "protobuf-support", + "thiserror", +] + +[[package]] +name = "protobuf-support" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d4d7b8601c814cfb36bcebb79f0e61e45e1e93640cf778837833bbed05c372" +dependencies = [ + "thiserror", +] + +[[package]] +name = "quote" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint", + "hmac", + "zeroize", +] + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "schemars" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 1.0.109", +] + +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" + +[[package]] +name = "serde" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-cw-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75d32da6b8ed758b7d850b6c3c08f1d7df51a4df3cb201296e63e34a78e99d4" +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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a62a1fad1e1828b24acac8f2b468971dade7b8c3c2e672bcadefefb1f8c137" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_bytes" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "serde_derive_internals" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "serde_json" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[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 = "skip" +version = "0.1.0" +dependencies = [ + "astroport", + "cosmos-sdk-proto 0.19.0", + "cosmwasm-schema", + "cosmwasm-std", + "ibc-proto", + "neutron-proto", + "osmosis-std", +] + +[[package]] +name = "skip-swap-entry-point" +version = "0.1.0" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus 1.0.1", + "cw-utils 1.0.1", + "skip", + "test-case", + "thiserror", +] + +[[package]] +name = "skip-swap-neutron-astroport-swap" +version = "0.1.0" +dependencies = [ + "astroport", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.0.1", + "cw-utils 1.0.1", + "skip", + "test-case", + "thiserror", +] + +[[package]] +name = "skip-swap-neutron-ibc-transfer" +version = "0.1.0" +dependencies = [ + "cosmos-sdk-proto 0.19.0", + "cosmwasm-std", + "cw-storage-plus 1.0.1", + "neutron-proto", + "neutron-sdk", + "prost", + "skip", + "test-case", + "thiserror", +] + +[[package]] +name = "skip-swap-osmosis-ibc-transfer" +version = "0.1.0" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus 1.0.1", + "ibc-proto", + "prost", + "serde-cw-value", + "serde-json-wasm 0.5.1", + "skip", + "test-case", + "thiserror", +] + +[[package]] +name = "skip-swap-osmosis-poolmanager-swap" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils 1.0.1", + "osmosis-std", + "skip", + "test-case", + "thiserror", +] + +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "subtle-encoding" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcb1ed7b8330c5eed5441052651dd7a12c75e2ed88f2ec024ae1fa3a5e59945" +dependencies = [ + "zeroize", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tendermint-proto" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5895470f28c530f8ae8c4071bf8190304ce00bd131d25e81730453124a3375c" +dependencies = [ + "bytes", + "flex-error", + "num-derive", + "num-traits", + "prost", + "prost-types", + "serde", + "serde_bytes", + "subtle-encoding", + "time", +] + +[[package]] +name = "tendermint-proto" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce23c8ff0e6634eb4c3c4aeed45076dc97dac91aac5501a905a67fa222e165b" +dependencies = [ + "bytes", + "flex-error", + "num-derive", + "num-traits", + "prost", + "prost-types", + "serde", + "serde_bytes", + "subtle-encoding", + "time", +] + +[[package]] +name = "test-case" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1d6e7bde536b0412f20765b76e921028059adfd1b90d8974d33fd3c91b25df" +dependencies = [ + "test-case-macros", +] + +[[package]] +name = "test-case-core" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d10394d5d1e27794f772b6fc854c7e91a2dc26e2cbf807ad523370c2a59c0cee" +dependencies = [ + "cfg-if", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "test-case-macros" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeb9a44b1c6a54c1ba58b152797739dba2a83ca74e18168a68c980eb142f9404" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", + "test-case-core", +] + +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "time" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" +dependencies = [ + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +dependencies = [ + "time-core", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-ident" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..90ccc0a6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,47 @@ +[workspace] +resolver = "2" +members = [ + "contracts/entry-point", + "contracts/networks/neutron/*", + "contracts/networks/osmosis/*", + "packages/*", +] + +[workspace.package] +version = "0.1.0" +authors = ["Skip"] +edition = "2021" +rust-version = "1.68.0" +license = "TBD" +homepage = "https://skip.money/" +repository = "https://github.com/skip-mev/swap-contracts" +documentation = "https://github.com/skip-mev/swap-contracts#readme" +keywords = ["cosmwasm"] + +[workspace.dependencies] +astroport = "2.8" +cosmwasm-schema = "1.2" +cosmwasm-std = { version = "1.2", features = ["stargate"] } +cosmos-sdk-proto = { version = "0.19", default-features = false } +cw2 = "1.0" +cw-storage-plus = "1" +cw-utils = "1.0.1" +ibc-proto = { version = "0.32.0", default-features = false } +neutron-proto = { version = "0.1.1", default-features = false, features = ["cosmwasm"] } +neutron-sdk = "0.5" +osmosis-std = "0.15.3" +prost = "0.11" +serde-cw-value = "0.7.0" +serde-json-wasm = "0.5.1" +test-case = "3.1.0" +thiserror = "1" + +[profile.release] +codegen-units = 1 +debug = false +debug-assertions = false +incremental = false +lto = true +opt-level = 3 +overflow-checks = true +rpath = false diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..a8d16e47 --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +check: + cargo check --target wasm32-unknown-unknown + +clippy: + cargo +nightly clippy --tests + +fmt: + cargo +nightly fmt + +test: + cargo test --locked --workspace + +# copied from DAO DAO: +# https://github.com/DA0-DA0/polytone/blob/main/devtools/optimize.sh +optimize: + if [[ $(shell uname -m) =~ "arm64" ]]; then \ + docker run --rm -v "$(CURDIR)":/code \ + --mount type=volume,source="$(notdir $(CURDIR))_cache",target=/code/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + --platform linux/arm64 \ + cosmwasm/workspace-optimizer-arm64:0.13.0; else \ + docker run --rm -v "$(CURDIR)":/code \ + --mount type=volume,source="$(notdir $(CURDIR))_cache",target=/code/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + --platform linux/amd64 \ + cosmwasm/workspace-optimizer:0.13.0; fi diff --git a/README.md b/README.md new file mode 100644 index 00000000..fd3571fa --- /dev/null +++ b/README.md @@ -0,0 +1,162 @@ +![Skip Swap](skip_swirl.png "Skipping and Swapping") + +# Skip API Contracts + +The Skip Swap contracts in this repository are used in [Skip API](https://api-swagger.skip.money/) to enable any-to-any swaps as part of multi-chain workflows. + +Skip API is a unified REST API + SDK that helps developers create more seamless cross-chain experiences for their end users with IBC [(Inter-Blockchain Communication protocol)](https://ibcprotocol.dev/). + +Skip API is designed so that even developers who are new to IBC can offer incredible cross-chain experiences, like swaps and transfers between any two IBC-enabled chains and tokens in as few transactions as possible, with reliable multi-chain relaying, packet tracking, and more. + +# Overview + +The on-chain components of the swapping functionality consist of: +1. A main entry point contract +2. Chain/dex-specific swap adapter contracts +3. Chain-specific IBC transfer adapter contracts + + +## 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. Dispatches the swaps provided in the call data to the relevant swap adapter contracts +3. Verifies the amount out received from the swaps is greater than the minimum amount required by the caller after all fees have been subtracted (swap, ibc, affiliate) +4. 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 + +## Swap Adapter Contracts + +Swap Adapter contracts are developed and deployed for each swap venue supported by Skip Swap. The contracts are responsible for: +1. Taking the standardized Skip Swap entry point message format and converting it to the specific swap venue's format +2. Swapping by calling the swap venue's respective smart contract or module +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) + +## IBC Transfer Adapter Contracts + +IBC Transfer adapter contracts are developed and deployed for each chain supported by Skip Swap. The contracts are responsible for: +1. Dispatching the IBC transfer (with the appropriate IBC fees if required) +2. Failing the entire transaction if the IBC transfer errors on the swap chain (sending the caller back their original funds) +3. Refunding the caller on the swap chain if the IBC transfer errors or times out once it reaches the destination chain (also refunding unused IBC fees) + +# Repository Structure + +The repository is organized in the following way: +``` +│ +├── contracts/ <- Contains all contracts +│ ├── entry-point/ <- Contains source code and tests for entry point contract +│ └── networks/ <- Contains source code and tests for all network adapter contracts +│ ├── neutron/ +│ │ ├── ibc-transfer/ +│ │ └── swap/ +│ └── osmosis/ +│ ├── ibc-transfer/ +│ └── swap/ +│ +├── deployed-contracts/ <- Contains deployed contracts info for each network +│ ├── neutron/ +│ └── osmosis/ +│ +├── packages/ <- Contains all package code used by the contracts +│ └── skip/ +│ +├── scripts/ <- Contains all configs and deployment scripts +│ ├── configs/ +│ ├── deploy.py +│ └── requirements.txt +│ +├── README.md +├── Cargo.lock +├── Cargo.toml +├── Makefile +└── README.md +``` + +# Testing + +All tests can be found in the tests folder in each respective contract package. + +Run all tests in the repo: +```bash +make test +``` + +Note: Due to the nature of the adapter contracts using stargate messages and interacting with chain-specific modules, integration testing is conducted on the respective testnets. See Deployment section for deployment instructions. + +# Development Processes + +The repository's CI is triggered on pull requests and will fail if any error or warnings appear running the `check`, `clippy`, and `fmt` commands found in the Makefile. + +Each command and how to run them are as follows: + +`cargo check --target wasm32-unknown-unknown` is used to compile the contracts and verify they are valid wasm: +``` bash +make check +``` + +`clippy` is used for linting: +``` bash +make clippy +``` + +`rustfmt` is used for formatting: +``` bash +make fmt +``` + +# Deployment + +To deploy the Skip Swap contracts, the steps are as follows: + +1. Build the optimized wasm bytecode of the contracts by running (they will appear in an artifacts folder): + + ``` bash + make optimize + ``` + +2. Ensure you have python 3.10 installed to run the deploy script. Download python 3.10 if you don't have it installed. + ``` bash + python3.10 --version + ``` + +3. Go into the scripts directory and create a virtual environment to download the python dependencies: + ``` bash + cd scripts + python3.10 -m venv venv + ``` + +4. Activate virtual environment, (venv) will show on left-hand side of shell + ``` bash + source venv/bin/activate + ``` + +5. Install all the dependencies: + ``` + pip install -r requirements.txt + ``` + +6. Add the mnemonic of the deployer address in the respsective chain's config toml file (located in configs folder): + + ``` toml + # Enter your mnemonic here + MNEMONIC = "" + ``` + +7. Run the deploy script with the following format (changing the chain [options: osmosis, neutron] and network [options: testnet, mainnet] depending on what is to be deployed): + ``` bash + python deploy.py + ``` + + Example: + ``` bash + python deploy.py osmosis testnet + ``` + +8. After running the deploy script, a toml file will be added/updated in the deployed-contracts/{CHAIN} folder with all relevant info for the deployment. + +# About Skip + +Skip helps developers provide extraordinary user experiences across all stages of the transaction lifecycle, from transaction construction, through cross-chain relaying + tracking, to block construction. \ No newline at end of file diff --git a/contracts/entry-point/Cargo.toml b/contracts/entry-point/Cargo.toml new file mode 100644 index 00000000..e274634c --- /dev/null +++ b/contracts/entry-point/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "skip-swap-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-std = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +skip = { path = "../../packages/skip" } +thiserror = { workspace = true } + +[dev-dependencies] +test-case = { workspace = true } \ No newline at end of file diff --git a/contracts/entry-point/src/contract.rs b/contracts/entry-point/src/contract.rs new file mode 100644 index 00000000..8b8009f0 --- /dev/null +++ b/contracts/entry-point/src/contract.rs @@ -0,0 +1,123 @@ +use crate::{ + error::ContractResult, + execute::{execute_post_swap_action, execute_swap_and_action}, + query::{query_ibc_transfer_adapter_contract, query_swap_venue_adapter_contract}, + state::{IBC_TRANSFER_CONTRACT_ADDRESS, SWAP_VENUE_MAP}, +}; +use cosmwasm_std::{ + entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, +}; +use skip::entry_point::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +/////////////////// +/// INSTANTIATE /// +/////////////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> ContractResult { + // Create response object to return + let mut response: Response = Response::new().add_attribute("action", "instantiate"); + + // 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_address = deps + .api + .addr_validate(&swap_venue.adapter_contract_address)?; + + // Insert the swap contract address into the map, keyed by the venue name + SWAP_VENUE_MAP.save( + deps.storage, + &swap_venue.name, + &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_address = + deps.api.addr_validate(&msg.ibc_transfer_contract_address)?; + + // Store the ibc transfer adapter contract address + IBC_TRANSFER_CONTRACT_ADDRESS.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); + + 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::SwapAndAction { + fee_swap, + user_swap, + min_coin, + timeout_timestamp, + post_swap_action, + affiliates, + } => execute_swap_and_action( + deps, + env, + info, + fee_swap, + user_swap, + min_coin, + timeout_timestamp, + post_swap_action, + affiliates, + ), + ExecuteMsg::PostSwapAction { + min_coin, + timeout_timestamp, + post_swap_action, + affiliates, + } => execute_post_swap_action( + deps, + env, + info, + min_coin, + timeout_timestamp, + post_swap_action, + affiliates, + ), + } +} + +///////////// +/// 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/entry-point/src/error.rs b/contracts/entry-point/src/error.rs new file mode 100644 index 00000000..6bf86c53 --- /dev/null +++ b/contracts/entry-point/src/error.rs @@ -0,0 +1,77 @@ +use cosmwasm_std::{OverflowError, StdError}; +use thiserror::Error; + +pub type ContractResult = core::result::Result; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + /////////////// + /// GENERAL /// + /////////////// + + #[error(transparent)] + Std(#[from] StdError), + + #[error(transparent)] + Overflow(#[from] OverflowError), + + #[error(transparent)] + Payment(#[from] cw_utils::PaymentError), + + #[error("Unauthorized")] + Unauthorized, + + #[error("Timeout Timestamp Less Than Current Timestamp")] + Timeout, + + //////////////// + /// FEE SWAP /// + //////////////// + + #[error("Fee Swap Not Allowed: Post Swap Action Is Not An IBC Transfer")] + FeeSwapNotAllowed, + + #[error("Fee Swap Operations Empty")] + FeeSwapOperationsEmpty, + + #[error("Fee Swap Coin In Denom Differs From Coin Sent To Contract")] + FeeSwapCoinInDenomMismatch, + + #[error("Fee Swap Coin In Denom Differs From First Swap Operation Denom In")] + FeeSwapOperationsCoinInDenomMismatch, + + #[error("Fee Swap Coin Out Denom Differs From Last Denom Out In Swap Operations")] + FeeSwapOperationsCoinOutDenomMismatch, + + ///////////////// + /// USER SWAP /// + ///////////////// + + #[error("User Swap Operations Empty")] + UserSwapOperationsEmpty, + + #[error("User Swap Coin In Denom Differs From Coin Sent To Contract")] + UserSwapCoinInDenomMismatch, + + #[error("User Swap Coin In Denom Differs From First Swap Operation Denom In")] + UserSwapOperationsCoinInDenomMismatch, + + #[error("User Swap Last Swap Operation Denom Out Differs From Min Coin Out Denom")] + UserSwapOperationsMinCoinDenomMismatch, + + #[error("User Swap Coin In Amount Is Greater Than The Remaining Coin Received")] + UserSwapCoinInGreaterThanRemainingReceived, + + //////////////////////// + /// POST SWAP ACTION /// + //////////////////////// + + #[error("Received Less Coin From Swaps Than Minimum Coin Required")] + ReceivedLessCoinFromSwapsThanMinCoin, + + #[error("Transfer Out Coin Less Than Minimum Required After Affiliate Fees")] + TransferOutCoinLessThanMinAfterAffiliateFees, + + #[error("Transfer Out Coin Less Than Minimum Required After IBC Fees")] + TransferOutCoinLessThanMinAfterIbcFees, +} diff --git a/contracts/entry-point/src/execute.rs b/contracts/entry-point/src/execute.rs new file mode 100644 index 00000000..5b889a33 --- /dev/null +++ b/contracts/entry-point/src/execute.rs @@ -0,0 +1,476 @@ +use crate::{ + error::{ContractError, ContractResult}, + state::{IBC_TRANSFER_CONTRACT_ADDRESS, SWAP_VENUE_MAP}, +}; +use cosmwasm_std::{ + to_binary, Addr, BankMsg, Binary, Coin, DepsMut, Env, MessageInfo, Response, Uint128, WasmMsg, +}; +use cw_utils::one_coin; +use skip::{ + entry_point::{Affiliate, ExecuteMsg, PostSwapAction}, + ibc::{ExecuteMsg as IbcTransferExecuteMsg, IbcFeeMap, IbcInfo, IbcTransfer}, + swap::{ + ExecuteMsg as SwapExecuteMsg, QueryMsg as SwapQueryMsg, SwapExactCoinIn, SwapExactCoinOut, + }, +}; + +/////////////////////////// +/// 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, + fee_swap: Option, + user_swap: SwapExactCoinIn, + min_coin: Coin, + timeout_timestamp: u64, + post_swap_action: PostSwapAction, + affiliates: Vec, +) -> ContractResult { + // Create a response object to return + let mut response: Response = Response::new().add_attribute("action", "execute_swap_and_action"); + + // Error if the current block time is greater than the timeout timestamp + if env.block.time.nanos() > timeout_timestamp { + return Err(ContractError::Timeout); + } + + // Get coin sent to the contract from the MessageInfo + // Use as a tank to decrease from for the fee swap in amount if it exists + // Then use it as the coin in for the user swap if one is not provided + // Error if there is not exactly one coin sent to the contract + let mut remaining_coin_received = one_coin(&info)?; + + // Process the fee swap if it exists + if let Some(fee_swap) = fee_swap { + // Error if the post swap action is not an ibc transfer + // since an ibc transfer is the only action that requires a fee swap + if !matches!(post_swap_action, PostSwapAction::IbcTransfer { .. }) { + return Err(ContractError::FeeSwapNotAllowed); + } + + // Create the fee swap message + // NOTE: this call mutates the user swap coin by subtracting the fee swap in amount + let fee_swap_msg = + verify_and_create_fee_swap_msg(&deps, fee_swap, &mut remaining_coin_received)?; + + // Add the fee swap message to the response + response = response + .add_message(fee_swap_msg) + .add_attribute("action", "dispatch_fee_swap"); + } + + // Create the user swap message + let user_swap_msg = verify_and_create_user_swap_msg( + &deps, + user_swap, + remaining_coin_received, + &min_coin.denom, + )?; + + // Create the transfer message + let post_swap_action_msg = WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_binary(&ExecuteMsg::PostSwapAction { + min_coin, + timeout_timestamp, + post_swap_action, + affiliates, + })?, + funds: vec![], + }; + + // Add the user swap message and post swap action message to the response + Ok(response + .add_message(user_swap_msg) + .add_message(post_swap_action_msg) + .add_attribute("action", "dispatch_user_swap_and_post_swap_action")) +} + +// 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_coin: Coin, + timeout_timestamp: u64, + post_swap_action: PostSwapAction, + 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_post_swap_action"); + + // Get contract balance of min out coin immediately after swap + // for fee deduction and transfer out amount enforcement + let transfer_out_coin_contract_balance_after_swaps = deps + .querier + .query_balance(&env.contract.address, &min_coin.denom)?; + + // Error if the contract balance is less than the min out coin amount + if transfer_out_coin_contract_balance_after_swaps.amount < min_coin.amount { + return Err(ContractError::ReceivedLessCoinFromSwapsThanMinCoin); + } + + // Mutable copy of the transfer out coin to subtract fees from + // to become the final transfer out coin we send to the user + let mut transfer_out_coin = transfer_out_coin_contract_balance_after_swaps.clone(); + + // If affiliates exist, create the affiliate fee messages and add them to the + // response, decreasing the transfer out coin amount by each 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, + &transfer_out_coin_contract_balance_after_swaps, + affiliate, + )?; + + // Subtract the affiliate fee from the transfer out coin + transfer_out_coin.amount = transfer_out_coin.amount.checked_sub(affiliate_fee_amount)?; + + // Create the affiliate fee bank send message + let affiliate_fee_msg = BankMsg::Send { + to_address: affiliate.address.clone(), + amount: vec![Coin { + denom: transfer_out_coin_contract_balance_after_swaps.denom.clone(), + amount: affiliate_fee_amount, + }], + }; + + // Add the affiliate fee message and logs to the response + response = 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); + } + + // If affiliates exist, then error if the transfer out coin amount + // is less than the min coin amount after affiliate fees + if !affiliates.is_empty() && transfer_out_coin.amount < min_coin.amount { + return Err(ContractError::TransferOutCoinLessThanMinAfterAffiliateFees); + } + + match post_swap_action { + PostSwapAction::BankSend { to_address } => { + // Create the bank send message + let bank_send_msg = + verify_and_create_bank_send_msg(deps, to_address, transfer_out_coin)?; + + // Add the bank send message to the response + response = response + .add_message(bank_send_msg) + .add_attribute("action", "dispatch_post_swap_bank_send"); + } + PostSwapAction::IbcTransfer { ibc_info } => { + // Enforce min out w/ ibc fees and create the IBC Transfer adapter contract call message + let ibc_transfer_adapter_msg = verify_and_create_ibc_transfer_adapter_msg( + deps, + min_coin, + timeout_timestamp, + ibc_info, + transfer_out_coin, + )?; + + // Add the IBC transfer message to the response + response = response + .add_message(ibc_transfer_adapter_msg) + .add_attribute("action", "dispatch_post_swap_ibc_transfer"); + } + PostSwapAction::ContractCall { + contract_address, + msg, + } => { + // Verify and create the contract call message + let contract_call_msg = verify_and_create_contract_call_msg( + deps, + contract_address, + msg, + transfer_out_coin, + )?; + + // Add the contract call message to the response + response = response + .add_message(contract_call_msg) + .add_attribute("action", "dispatch_post_swap_contract_call"); + } + }; + + Ok(response) +} + +//////////////////////// +/// HELPER FUNCTIONS /// +//////////////////////// + +// 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, + transfer_out_coin_contract_balance_after_swaps: &Coin, + affiliate: &Affiliate, +) -> ContractResult { + // Verify the affiliate address is valid + deps.api.addr_validate(&affiliate.address)?; + + // Get the affiliate fee amount by multiplying the transfer out coin amount + // immediately after the swaps by the affiliate basis points fee divided by 10000 + let affiliate_fee_amount = transfer_out_coin_contract_balance_after_swaps + .amount + .multiply_ratio(affiliate.basis_points_fee, Uint128::new(10000)); + + Ok(affiliate_fee_amount) +} + +// POST SWAP ACTION MESSAGE HELPER FUNCTIONS + +// Do min transfer coin out verification, +// Then create and return a bank send message +fn verify_and_create_bank_send_msg( + deps: DepsMut, + to_address: String, + transfer_out_coin: Coin, +) -> ContractResult { + // Error if the destination address is not a valid address on the current chain + deps.api.addr_validate(&to_address)?; + + // Create the bank send message + let bank_send_msg = BankMsg::Send { + to_address, + amount: vec![transfer_out_coin], + }; + + Ok(bank_send_msg) +} + +// Do min transfer coin out and ibc fee verification, +// Then create and return a message that calls the IBC Transfer adapter contract +fn verify_and_create_ibc_transfer_adapter_msg( + deps: DepsMut, + min_coin: Coin, + timeout_timestamp: u64, + ibc_info: IbcInfo, + mut transfer_out_coin: Coin, +) -> ContractResult { + // Validates recover address, errors if invalid + deps.api.addr_validate(&ibc_info.recover_address)?; + + // Create the ibc_fees map from the given recv_fee, ack_fee, and timeout_fee + let ibc_fees: IbcFeeMap = ibc_info.fee.clone().try_into()?; + + // Get the amount of the IBC fee payment that matches + // the denom of the ibc transfer out coin. + // If there is no denom match, then default to zero. + let transfer_out_coin_ibc_fee_amount = ibc_fees.get_amount(&min_coin.denom); + + // Subtract the IBC fee amount from the transfer out coin + transfer_out_coin.amount = transfer_out_coin + .amount + .checked_sub(transfer_out_coin_ibc_fee_amount)?; + + // Check if the swap out amount after IBC fee is greater than the minimum amount out + // If it is, then send the IBC transfer, otherwise, return an error + if transfer_out_coin.amount < min_coin.amount { + return Err(ContractError::TransferOutCoinLessThanMinAfterIbcFees); + } + + // Calculate the funds to send to the IBC transfer contract + // (which is the transfer out coin plus the IBC fee amounts) + // using a map for convenience, and then converting to a vector of coins + let mut ibc_msg_funds_map = ibc_fees; + ibc_msg_funds_map.add_coin(&transfer_out_coin)?; + + // Convert the map to a vector of coins + let ibc_msg_funds: Vec = ibc_msg_funds_map.into(); + + // Create the IBC transfer message + let ibc_transfer_msg: IbcTransferExecuteMsg = IbcTransfer { + info: ibc_info, + coin: transfer_out_coin, + timeout_timestamp, + } + .into(); + + // Get the IBC transfer adapter contract address + let ibc_transfer_contract_address = IBC_TRANSFER_CONTRACT_ADDRESS.load(deps.storage)?; + + // Send the IBC transfer by calling the IBC transfer contract + let ibc_msg = WasmMsg::Execute { + contract_addr: ibc_transfer_contract_address.to_string(), + msg: to_binary(&ibc_transfer_msg)?, + funds: ibc_msg_funds, + }; + + Ok(ibc_msg) +} + +// Verifies the contract address is valid, if so +// then creates and returns a contract call message +fn verify_and_create_contract_call_msg( + deps: DepsMut, + contract_address: String, + msg: Binary, + transfer_out_coin: Coin, +) -> ContractResult { + // Verify the contract address is valid, error if invalid + deps.api.addr_validate(&contract_address)?; + + // Create the contract call message + let contract_call_msg = WasmMsg::Execute { + contract_addr: contract_address, + msg, + funds: vec![transfer_out_coin], + }; + + Ok(contract_call_msg) +} + +// SWAP MESSAGE HELPER FUNCTIONS + +// Creates the user swap message and returns it +fn verify_and_create_user_swap_msg( + deps: &DepsMut, + user_swap: SwapExactCoinIn, + remaining_coin_received: Coin, + min_coin_denom: &str, +) -> ContractResult { + // Verify the swap operations are not empty + let (Some(first_op), Some(last_op)) = (user_swap.operations.first(), user_swap.operations.last()) else { + return Err(ContractError::UserSwapOperationsEmpty); + }; + + // Set the user swap coin in to the remaining coin received if it is not provided + // Otherwise, use the provided user swap coin in, erroring if it doesn't pass validation + let user_swap_coin_in = match user_swap.coin_in.clone() { + Some(coin_in) => { + // Verify the coin_in denom is the same as the remaining coin received denom + if coin_in.denom != remaining_coin_received.denom { + return Err(ContractError::UserSwapCoinInDenomMismatch); + } + + // Verify the coin_in amount is less than or equal to the remaining coin received amount + if coin_in.amount > remaining_coin_received.amount { + return Err(ContractError::UserSwapCoinInGreaterThanRemainingReceived); + } + + coin_in + } + None => remaining_coin_received, + }; + + // Verify the user_swap_coin is the same denom as the first swap operation denom in + if user_swap_coin_in.denom != first_op.denom_in { + return Err(ContractError::UserSwapOperationsCoinInDenomMismatch); + } + + // Verify the last swap operation denom out is the same as the min coin denom + if min_coin_denom != last_op.denom_out { + return Err(ContractError::UserSwapOperationsMinCoinDenomMismatch); + } + + // Get swap adapter contract address from venue name + let user_swap_adapter_contract_address = + SWAP_VENUE_MAP.load(deps.storage, &user_swap.swap_venue_name)?; + + // Create the user swap message args + let user_swap_msg_args: SwapExecuteMsg = user_swap.into(); + + // Create the user swap message + let user_swap_msg = WasmMsg::Execute { + contract_addr: user_swap_adapter_contract_address.to_string(), + msg: to_binary(&user_swap_msg_args)?, + funds: vec![user_swap_coin_in], + }; + + Ok(user_swap_msg) +} + +// Creates the fee swap message and returns it +// Also deducts the fee swap in amount from the mutable user swap coin +fn verify_and_create_fee_swap_msg( + deps: &DepsMut, + fee_swap: SwapExactCoinOut, + remaining_coin_received: &mut Coin, +) -> ContractResult { + // Verify the swap operations are not empty + let (Some(first_op), Some(last_op)) = (fee_swap.operations.first(), fee_swap.operations.last()) else { + return Err(ContractError::FeeSwapOperationsEmpty); + }; + + // Verify the fee swap coin out is the same denom as the last swap operation denom out + if fee_swap.coin_out.denom != last_op.denom_out { + return Err(ContractError::FeeSwapOperationsCoinOutDenomMismatch); + } + + // Get swap adapter contract address from venue name + let fee_swap_adapter_contract_address = + SWAP_VENUE_MAP.load(deps.storage, &fee_swap.swap_venue_name)?; + + // Query the swap adapter to get the coin in needed for the fee swap + let fee_swap_coin_in = + query_swap_coin_in(deps, &fee_swap_adapter_contract_address, fee_swap.clone())?; + + // Verify the fee_swap_coin_in is the same denom as the first swap operation denom in + if fee_swap_coin_in.denom != first_op.denom_in { + return Err(ContractError::FeeSwapOperationsCoinInDenomMismatch); + } + + // Verify the fee swap in denom is the same as the denom received from the message to the contract + if fee_swap_coin_in.denom != remaining_coin_received.denom { + return Err(ContractError::FeeSwapCoinInDenomMismatch); + } + + // Deduct the fee swap in amount from the swappable coin + // Error if swap requires more than the swappable coin amount + remaining_coin_received.amount = remaining_coin_received + .amount + .checked_sub(fee_swap_coin_in.amount)?; + + // Create the fee swap message args + let fee_swap_msg_args: SwapExecuteMsg = fee_swap.into(); + + // Create the fee swap message + let fee_swap_msg = WasmMsg::Execute { + contract_addr: fee_swap_adapter_contract_address.to_string(), + msg: to_binary(&fee_swap_msg_args)?, + funds: vec![fee_swap_coin_in], + }; + + Ok(fee_swap_msg) +} + +// QUERY HELPER FUNCTIONS + +// Unexposed query helper function that queries the swap adapter contract to get the +// coin in needed for the fee swap. Verifies the fee swap in denom is the same as the +// swap coin denom from the message. Returns the fee swap coin in. +fn query_swap_coin_in( + deps: &DepsMut, + swap_adapter_contract_address: &Addr, + fee_swap: SwapExactCoinOut, +) -> ContractResult { + // Query the swap adapter to get the coin in needed for the fee swap + let fee_swap_coin_in: Coin = deps.querier.query_wasm_smart( + swap_adapter_contract_address, + &SwapQueryMsg::SimulateSwapExactCoinOut { + coin_out: fee_swap.coin_out, + swap_operations: fee_swap.operations, + }, + )?; + + Ok(fee_swap_coin_in) +} diff --git a/contracts/entry-point/src/lib.rs b/contracts/entry-point/src/lib.rs new file mode 100644 index 00000000..512e6c21 --- /dev/null +++ b/contracts/entry-point/src/lib.rs @@ -0,0 +1,5 @@ +pub mod contract; +pub mod error; +pub mod execute; +pub mod query; +pub mod state; diff --git a/contracts/entry-point/src/query.rs b/contracts/entry-point/src/query.rs new file mode 100644 index 00000000..26cdf6f3 --- /dev/null +++ b/contracts/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 { + SWAP_VENUE_MAP.load(deps.storage, &name) +} + +// Queries the IBC transfer adapter contract address and returns it if it exists +pub fn query_ibc_transfer_adapter_contract(deps: Deps) -> StdResult { + IBC_TRANSFER_CONTRACT_ADDRESS.load(deps.storage) +} diff --git a/contracts/entry-point/src/state.rs b/contracts/entry-point/src/state.rs new file mode 100644 index 00000000..1d6d75bf --- /dev/null +++ b/contracts/entry-point/src/state.rs @@ -0,0 +1,5 @@ +use cosmwasm_std::Addr; +use cw_storage_plus::{Item, Map}; + +pub const SWAP_VENUE_MAP: Map<&str, Addr> = Map::new("swap_venue_map"); +pub const IBC_TRANSFER_CONTRACT_ADDRESS: Item = Item::new("ibc_transfer_contract_address"); diff --git a/contracts/entry-point/tests/test_execute_post_swap_action.rs b/contracts/entry-point/tests/test_execute_post_swap_action.rs new file mode 100644 index 00000000..9de142f7 --- /dev/null +++ b/contracts/entry-point/tests/test_execute_post_swap_action.rs @@ -0,0 +1,583 @@ +use cosmwasm_std::{ + testing::{mock_dependencies_with_balances, mock_env, mock_info}, + to_binary, Addr, BankMsg, Coin, + ReplyOn::Never, + SubMsg, Timestamp, Uint128, WasmMsg, +}; +use skip::{ + entry_point::{Affiliate, ExecuteMsg, PostSwapAction}, + ibc::{ExecuteMsg as IbcTransferExecuteMsg, IbcFee, IbcInfo}, +}; +use skip_swap_entry_point::{error::ContractError, state::IBC_TRANSFER_CONTRACT_ADDRESS}; +use test_case::test_case; + +/* +Test Cases: + +Expect Response + // General + - Bank Send + - Ibc Transfer + - Contract Call + + // With IBC Fees + - Ibc Transfer w/ IBC Fees of different denom than min coin + - Ibc Transfer w/ IBC Fees of same denom as min coin + + // With Affiliates + - Bank Send w/ Affiliate + - Contract Call w/ Affiliate + - Ibc Transfer w/ IBC Fees of different denom than min coin w/ Affiliate + - Ibc Transfer w/ IBC Fees of same denom as min coin w/ Affiliate + +Expect Error + - Bank Send Timeout + - Ibc Transfer w/ Affiliates Decreasing user transfer below min coin + - Ibc Transfer w/ IBC Fees Decreasing user transfer below min coin + - Received Less From Swap Than Min Coin + - Unauthorized Caller + */ + +// Define test parameters +struct Params { + caller: String, + min_coin: Coin, + post_swap_action: PostSwapAction, + affiliates: Vec, + expected_messages: Vec, + expected_error: Option, +} + +// Test execute_post_swap_action +#[test_case( + Params { + caller: "entry_point".to_string(), + min_coin: Coin::new(1_000_000, "osmo"), + post_swap_action: PostSwapAction::BankSend { + to_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5".to_string(), + }, + affiliates: vec![], + expected_messages: vec![SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5".to_string(), + amount: vec![Coin::new(1_000_000, "osmo")], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Bank Send")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_coin: Coin::new(1_000_000, "osmo"), + post_swap_action: PostSwapAction::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![], + timeout_fee: vec![], + }, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + }, + affiliates: vec![], + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "ibc_transfer_adapter".to_string(), + msg: to_binary(&IbcTransferExecuteMsg::IbcTransfer { + info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![], + timeout_fee: vec![], + }, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + coin: Coin::new(1_000_000, "osmo"), + timeout_timestamp: 101, + }) + .unwrap(), + funds: vec![Coin::new(1_000_000, "osmo")], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Ibc Transfer")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_coin: Coin::new(1_000_000, "osmo"), + post_swap_action: PostSwapAction::ContractCall { + contract_address: "contract_call_address".to_string(), + msg: to_binary(&"contract_call_msg").unwrap(), + }, + affiliates: vec![], + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "contract_call_address".to_string(), + msg: to_binary(&"contract_call_msg").unwrap(), + funds: vec![Coin::new(1_000_000, "osmo")], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Contract Call" +)] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_coin: Coin::new(1_000_000, "osmo"), + post_swap_action: PostSwapAction::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + }, + affiliates: vec![], + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "ibc_transfer_adapter".to_string(), + msg: to_binary(&IbcTransferExecuteMsg::IbcTransfer { + info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + coin: Coin::new(1_000_000, "osmo"), + timeout_timestamp: 101, + }) + .unwrap(), + funds: vec![ + Coin::new(1_000_000, "osmo"), + Coin::new(200_000, "untrn"), + ], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Ibc Transfer w/ IBC Fees of different denom than min coin")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_coin: Coin::new(800_000, "untrn"), + post_swap_action: PostSwapAction::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + }, + affiliates: vec![], + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "ibc_transfer_adapter".to_string(), + msg: to_binary(&IbcTransferExecuteMsg::IbcTransfer { + info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + coin: Coin::new(800_000, "untrn"), + timeout_timestamp: 101, + }) + .unwrap(), + funds: vec![Coin::new(1_000_000, "untrn")], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Ibc Transfer w/ IBC Fees of same denom as min coin")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_coin: Coin::new(900_000, "osmo"), + post_swap_action: PostSwapAction::BankSend { + to_address: "swapper".to_string(), + }, + affiliates: vec![Affiliate { + address: "affiliate".to_string(), + basis_points_fee: Uint128::new(1000), + }], + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "affiliate".to_string(), + amount: vec![Coin::new(100_000, "osmo")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "swapper".to_string(), + amount: vec![Coin::new(900_000, "osmo")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Bank Send w/ Affiliate")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_coin: Coin::new(900_000, "osmo"), + post_swap_action: PostSwapAction::ContractCall { + contract_address: "contract_call".to_string(), + msg: to_binary(&"contract_call_msg").unwrap(), + }, + affiliates: vec![Affiliate { + address: "affiliate".to_string(), + basis_points_fee: Uint128::new(1000), + }], + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "affiliate".to_string(), + amount: vec![Coin::new(100_000, "osmo")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "contract_call".to_string(), + msg: to_binary(&"contract_call_msg").unwrap(), + funds: vec![Coin::new(900_000, "osmo")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Contract Call w/ Affiliate")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_coin: Coin::new(900_000, "osmo"), + post_swap_action: PostSwapAction::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }, + recover_address: "recover".to_string(), + }, + }, + affiliates: vec![Affiliate { + address: "affiliate".to_string(), + basis_points_fee: Uint128::new(1000), + }], + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "affiliate".to_string(), + amount: vec![Coin::new(100_000, "osmo")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "ibc_transfer_adapter".to_string(), + msg: to_binary(&IbcTransferExecuteMsg::IbcTransfer { + info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }, + recover_address: "recover".to_string(), + }, + coin: Coin::new(900_000, "osmo"), + timeout_timestamp: 101, + }) + .unwrap(), + funds: vec![Coin::new(900_000, "osmo"), Coin::new(200_000, "untrn")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Ibc Transfer w/ IBC Fees of different denom than min coin w/ Affiliate")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_coin: Coin::new(700_000, "untrn"), + post_swap_action: PostSwapAction::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }, + recover_address: "recover".to_string(), + }, + }, + affiliates: vec![Affiliate { + address: "affiliate".to_string(), + basis_points_fee: Uint128::new(1000), + }], + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "affiliate".to_string(), + amount: vec![Coin::new(100_000, "untrn")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "ibc_transfer_adapter".to_string(), + msg: to_binary(&IbcTransferExecuteMsg::IbcTransfer { + info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }, + recover_address: "recover".to_string(), + }, + coin: Coin::new(700_000, "untrn"), + timeout_timestamp: 101, + }) + .unwrap(), + funds: vec![Coin::new(900_000, "untrn")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Ibc Transfer w/ IBC Fees of same denom as min coin w/ Affiliate")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_coin: Coin::new(950_000, "osmo"), + post_swap_action: PostSwapAction::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }, + recover_address: "recover".to_string(), + }, + }, + affiliates: vec![Affiliate { + address: "affiliate".to_string(), + basis_points_fee: Uint128::new(1000), + }], + expected_messages: vec![], + expected_error: Some(ContractError::TransferOutCoinLessThanMinAfterAffiliateFees), + }; + "Ibc Transfer w/ Affiliates Decreasing user transfer below min coin - Expect Error")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_coin: Coin::new(900_000, "untrn"), + post_swap_action: PostSwapAction::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }, + recover_address: "recover".to_string(), + }, + }, + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::TransferOutCoinLessThanMinAfterIbcFees), + }; + "Ibc Transfer w/ IBC Fees Decreasing user transfer below min coin - Expect Error")] +#[test_case( + Params { + caller: "entry_point".to_string(), + min_coin: Coin::new(1_100_000, "untrn"), + post_swap_action: PostSwapAction::BankSend { + to_address: "swapper".to_string(), + }, + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::ReceivedLessCoinFromSwapsThanMinCoin), + }; + "Received Less From Swap Than Min Coin - Expect Error")] +#[test_case( + Params { + caller: "unauthorized".to_string(), + min_coin: Coin::new(1_100_000, "untrn"), + post_swap_action: PostSwapAction::BankSend { + to_address: "swapper".to_string(), + }, + affiliates: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Unauthorized {}), + }; + "Unauthorized Caller - Expect Error")] +fn test_execute_post_swap_action(params: Params) { + // Create mock dependencies + let mut deps = mock_dependencies_with_balances(&[( + "entry_point", + &[Coin::new(1_000_000, "osmo"), Coin::new(1_000_000, "untrn")], + )]); + + // Create mock env with parameters that make testing easier + let mut env = mock_env(); + env.contract.address = Addr::unchecked("entry_point"); + env.block.time = Timestamp::from_nanos(100); + + // Create mock info with entry point contract address + let info = mock_info(¶ms.caller, &[]); + + // Store the ibc transfer adapter contract address + let ibc_transfer_adapter = Addr::unchecked("ibc_transfer_adapter"); + IBC_TRANSFER_CONTRACT_ADDRESS + .save(deps.as_mut().storage, &ibc_transfer_adapter) + .unwrap(); + + // Call execute_post_swap_action with the given test parameters + let res = skip_swap_entry_point::contract::execute( + deps.as_mut(), + env, + info, + ExecuteMsg::PostSwapAction { + min_coin: params.min_coin, + timeout_timestamp: 101, + post_swap_action: params.post_swap_action, + affiliates: params.affiliates, + }, + ); + + match res { + Ok(res) => { + // Assert the test did not expect an error + assert!( + params.expected_error.is_none(), + "expected test to error with {:?}, but it succeeded", + params.expected_error + ); + + // Assert the number of messages in the response is correct + assert_eq!( + res.messages.len(), + params.expected_messages.len(), + "expected {:?} messages, but got {:?}", + params.expected_messages.len(), + res.messages.len() + ); + + // Assert the messages in the response are correct + assert_eq!(res.messages, params.expected_messages,); + } + Err(err) => { + // Assert the test expected an error + assert!( + params.expected_error.is_some(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err, params.expected_error.unwrap()); + } + } +} diff --git a/contracts/entry-point/tests/test_execute_swap_and_action.rs b/contracts/entry-point/tests/test_execute_swap_and_action.rs new file mode 100644 index 00000000..c8dafa3b --- /dev/null +++ b/contracts/entry-point/tests/test_execute_swap_and_action.rs @@ -0,0 +1,952 @@ +use cosmwasm_std::{ + testing::{mock_dependencies_with_balances, mock_env, mock_info}, + to_binary, Addr, Coin, ContractResult, OverflowError, OverflowOperation, QuerierResult, + ReplyOn::Never, + SubMsg, SystemResult, Timestamp, WasmMsg, WasmQuery, +}; +use cw_utils::PaymentError::{MultipleDenoms, NoFunds}; +use skip::{ + entry_point::{Affiliate, ExecuteMsg, PostSwapAction}, + ibc::{IbcFee, IbcInfo}, + swap::{ExecuteMsg as SwapExecuteMsg, SwapExactCoinIn, SwapExactCoinOut, SwapOperation}, +}; +use skip_swap_entry_point::{error::ContractError, state::SWAP_VENUE_MAP}; +use test_case::test_case; + +/* +Test Cases: + +Expect Response + - User Swap + - Fee Swap And User Swap Using Leftover Coin + - Fee Swap And User Swap Using Specified Coin + +Expect Error + // Fee Swap + - Fee Swap Necessary Coin More Than Sent To Contract + - Fee Swap Required Denom In Not The Same As Coin Sent To Contract + - Fee Swap Required Denom In Not The Same As First Swap Operation Denom In + - Fee Swap Coin Out Denom Is Not The Same As Last Swap Operation Denom Out + - Fee Swap Without IBC Transfer Post Swap Action + + // User Swap + - User Swap Specified Coin More Than Remaining Coin Sent To Contract After Fee Swap + - User Swap Denom In Is Not The Same As Coin Sent To Contract + - User Swap Denom In Is Not The Same As First Swap Operation Denom In + - User Swap Last Swap Operation Denom Out Is Not The Same As Min Coin Out Denom + + // Invalid Coins Sent To Contract + - No Coins Sent To Contract + - More Than One Coin Sent To Contract + + // Empty Swap Operations + - Empty User Swap Operations + - Empty Fee Swap Operations + */ + +// Define test parameters +struct Params { + info_funds: Vec, + fee_swap: Option, + user_swap: SwapExactCoinIn, + min_coin: Coin, + timeout_timestamp: u64, + post_swap_action: PostSwapAction, + expected_messages: Vec, + expected_error: Option, +} + +// Test execute_swap_and_action +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "untrn"), + ], + fee_swap: None, + user_swap: SwapExactCoinIn { + swap_venue_name: "swap_venue_name".to_string(), + coin_in: None, + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + } + ], + }, + min_coin: Coin::new(1_000_000, "osmo"), + timeout_timestamp: 101, + post_swap_action: PostSwapAction::BankSend { + to_address: "to_address".to_string(), + }, + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_venue_adapter".to_string(), + msg: to_binary(&SwapExecuteMsg::Swap { + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + } + ], + }).unwrap(), + funds: vec![Coin::new(1_000_000, "untrn")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_binary(&ExecuteMsg::PostSwapAction { + min_coin: Coin::new(1_000_000, "osmo"), + timeout_timestamp: 101, + post_swap_action: PostSwapAction::BankSend { + to_address: "to_address".to_string(), + }, + affiliates: vec![], + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "User Swap")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "osmo"), + ], + fee_swap: Some( + SwapExactCoinOut { + swap_venue_name: "swap_venue_name".to_string(), + coin_out: Coin::new(200_000, "untrn"), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "osmo".to_string(), + denom_out: "untrn".to_string(), + } + ], + } + ), + user_swap: SwapExactCoinIn { + swap_venue_name: "swap_venue_name".to_string(), + coin_in: None, + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "uatom".to_string(), + } + ], + }, + min_coin: Coin::new(100_000, "uatom"), + timeout_timestamp: 101, + post_swap_action: PostSwapAction::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + }, + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_venue_adapter".to_string(), + msg: to_binary(&SwapExecuteMsg::Swap { + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "osmo".to_string(), + denom_out: "untrn".to_string(), + } + ], + }).unwrap(), + funds: vec![Coin::new(200_000, "osmo")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_venue_adapter".to_string(), + msg: to_binary(&SwapExecuteMsg::Swap { + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "uatom".to_string(), + } + ], + }).unwrap(), + funds: vec![Coin::new(800_000, "osmo")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_binary(&ExecuteMsg::PostSwapAction { + min_coin: Coin::new(100_000, "uatom"), + timeout_timestamp: 101, + post_swap_action: PostSwapAction::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + }, + affiliates: vec![], + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Fee Swap And User Swap Using Leftover Coin")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "osmo"), + ], + fee_swap: Some( + SwapExactCoinOut { + swap_venue_name: "swap_venue_name".to_string(), + coin_out: Coin::new(200_000, "untrn"), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "osmo".to_string(), + denom_out: "untrn".to_string(), + } + ], + } + ), + user_swap: SwapExactCoinIn { + swap_venue_name: "swap_venue_name".to_string(), + coin_in: Some(Coin::new(800_000, "osmo")), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "uatom".to_string(), + } + ], + }, + min_coin: Coin::new(100_000, "uatom"), + timeout_timestamp: 101, + post_swap_action: PostSwapAction::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + }, + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_venue_adapter".to_string(), + msg: to_binary(&SwapExecuteMsg::Swap { + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "osmo".to_string(), + denom_out: "untrn".to_string(), + } + ], + }).unwrap(), + funds: vec![Coin::new(200_000, "osmo")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_venue_adapter".to_string(), + msg: to_binary(&SwapExecuteMsg::Swap { + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "uatom".to_string(), + } + ], + }).unwrap(), + funds: vec![Coin::new(800_000, "osmo")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "entry_point".to_string(), + msg: to_binary(&ExecuteMsg::PostSwapAction { + min_coin: Coin::new(100_000, "uatom"), + timeout_timestamp: 101, + post_swap_action: PostSwapAction::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + }, + affiliates: vec![], + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Fee Swap And User Swap Using Specified Coin")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "osmo"), + ], + fee_swap: Some( + SwapExactCoinOut { + swap_venue_name: "swap_venue_name".to_string(), + coin_out: Coin::new(200_000, "untrn"), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "osmo".to_string(), + denom_out: "untrn".to_string(), + } + ], + } + ), + user_swap: SwapExactCoinIn { + swap_venue_name: "swap_venue_name".to_string(), + coin_in: Some(Coin::new(900_000, "osmo")), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "uatom".to_string(), + } + ], + }, + min_coin: Coin::new(100_000, "uatom"), + timeout_timestamp: 101, + post_swap_action: PostSwapAction::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + }, + expected_messages: vec![], + expected_error: Some(ContractError::UserSwapCoinInGreaterThanRemainingReceived), + }; + "User Swap Specified Coin More Than Remaining Coin Sent To Contract After Fee Swap - Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(100_000, "osmo"), + ], + fee_swap: Some( + SwapExactCoinOut { + swap_venue_name: "swap_venue_name".to_string(), + coin_out: Coin::new(200_000, "untrn"), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "osmo".to_string(), + denom_out: "untrn".to_string(), + } + ], + } + ), + user_swap: SwapExactCoinIn { + swap_venue_name: "swap_venue_name".to_string(), + coin_in: Some(Coin::new(100_000, "osmo")), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "uatom".to_string(), + } + ], + }, + min_coin: Coin::new(100_000, "uatom"), + timeout_timestamp: 101, + post_swap_action: PostSwapAction::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + }, + expected_messages: vec![], + expected_error: Some(ContractError::Overflow(OverflowError { + operation: OverflowOperation::Sub, + operand1: "100000".to_string(), + operand2: "200000".to_string(), + })), + }; + "Fee Swap Necessary Coin More Than Sent To Contract - Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "uatom"), + ], + fee_swap: Some( + SwapExactCoinOut { + swap_venue_name: "swap_venue_name".to_string(), + coin_out: Coin::new(200_000, "untrn"), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "osmo".to_string(), + denom_out: "untrn".to_string(), + } + ], + } + ), + user_swap: SwapExactCoinIn { + swap_venue_name: "swap_venue_name".to_string(), + coin_in: Some(Coin::new(900_000, "osmo")), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "uatom".to_string(), + } + ], + }, + min_coin: Coin::new(100_000, "uatom"), + timeout_timestamp: 101, + post_swap_action: PostSwapAction::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + }, + expected_messages: vec![], + expected_error: Some(ContractError::FeeSwapCoinInDenomMismatch), + }; + "Fee Swap Required Denom In Not The Same As Coin Sent To Contract - Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "osmo"), + ], + fee_swap: Some( + SwapExactCoinOut { + swap_venue_name: "swap_venue_name".to_string(), + coin_out: Coin::new(200_000, "untrn"), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "uatom".to_string(), + denom_out: "untrn".to_string(), + } + ], + } + ), + user_swap: SwapExactCoinIn { + swap_venue_name: "swap_venue_name".to_string(), + coin_in: Some(Coin::new(900_000, "osmo")), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "uatom".to_string(), + } + ], + }, + min_coin: Coin::new(100_000, "uatom"), + timeout_timestamp: 101, + post_swap_action: PostSwapAction::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + }, + expected_messages: vec![], + expected_error: Some(ContractError::FeeSwapOperationsCoinInDenomMismatch), + }; + "Fee Swap Required Denom In Not The Same As First Swap Operation Denom In - Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "osmo"), + ], + fee_swap: Some( + SwapExactCoinOut { + swap_venue_name: "swap_venue_name".to_string(), + coin_out: Coin::new(200_000, "untrn"), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "uatom".to_string(), + denom_out: "osmo".to_string(), + } + ], + } + ), + user_swap: SwapExactCoinIn { + swap_venue_name: "swap_venue_name".to_string(), + coin_in: Some(Coin::new(900_000, "osmo")), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "uatom".to_string(), + } + ], + }, + min_coin: Coin::new(100_000, "uatom"), + timeout_timestamp: 101, + post_swap_action: PostSwapAction::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + }, + expected_messages: vec![], + expected_error: Some(ContractError::FeeSwapOperationsCoinOutDenomMismatch), + }; + "Fee Swap Coin Out Denom Is Not The Same As Last Swap Operation Denom Out - Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "uatom"), + ], + fee_swap: None, + user_swap: SwapExactCoinIn { + swap_venue_name: "swap_venue_name".to_string(), + coin_in: Some(Coin::new(900_000, "osmo")), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "uatom".to_string(), + } + ], + }, + min_coin: Coin::new(100_000, "uatom"), + timeout_timestamp: 101, + post_swap_action: PostSwapAction::BankSend { + to_address: "to_address".to_string(), + }, + expected_messages: vec![], + expected_error: Some(ContractError::UserSwapCoinInDenomMismatch), + }; + "User Swap Denom In Is Not The Same As Coin Sent To Contract - Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "osmo"), + ], + fee_swap: None, + user_swap: SwapExactCoinIn { + swap_venue_name: "swap_venue_name".to_string(), + coin_in: Some(Coin::new(900_000, "osmo")), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "untrn".to_string(), + denom_out: "uatom".to_string(), + } + ], + }, + min_coin: Coin::new(100_000, "uatom"), + timeout_timestamp: 101, + post_swap_action: PostSwapAction::BankSend { + to_address: "to_address".to_string(), + }, + expected_messages: vec![], + expected_error: Some(ContractError::UserSwapOperationsCoinInDenomMismatch), + }; + "User Swap Denom In Is Not The Same As First Swap Operation Denom In - Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "osmo"), + ], + fee_swap: None, + user_swap: SwapExactCoinIn { + swap_venue_name: "swap_venue_name".to_string(), + coin_in: Some(Coin::new(900_000, "osmo")), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "osmo".to_string(), + } + ], + }, + min_coin: Coin::new(100_000, "uatom"), + timeout_timestamp: 101, + post_swap_action: PostSwapAction::BankSend { + to_address: "to_address".to_string(), + }, + expected_messages: vec![], + expected_error: Some(ContractError::UserSwapOperationsMinCoinDenomMismatch), + }; + "User Swap Last Swap Operation Denom Out Is Not The Same As Min Coin Out Denom - Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "osmo"), + ], + fee_swap: Some( + SwapExactCoinOut { + swap_venue_name: "swap_venue_name".to_string(), + coin_out: Coin::new(200_000, "untrn"), + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "osmo".to_string(), + denom_out: "untrn".to_string(), + } + ], + } + ), + user_swap: SwapExactCoinIn { + swap_venue_name: "swap_venue_name".to_string(), + coin_in: None, + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "atom".to_string(), + } + ], + }, + min_coin: Coin::new(100_000, "atom"), + timeout_timestamp: 101, + post_swap_action: PostSwapAction::BankSend { + to_address: "to_address".to_string(), + }, + expected_messages: vec![], + expected_error: Some(ContractError::FeeSwapNotAllowed), + }; + "Fee Swap Without IBC Transfer Post Swap Action - Expect Error")] +#[test_case( + Params { + info_funds: vec![], + fee_swap: None, + user_swap: SwapExactCoinIn { + swap_venue_name: "swap_venue_name".to_string(), + coin_in: None, + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + } + ], + }, + min_coin: Coin::new(1_000_000, "osmo"), + timeout_timestamp: 101, + post_swap_action: PostSwapAction::BankSend { + to_address: "to_address".to_string(), + }, + expected_messages: vec![], + expected_error: Some(ContractError::Payment(NoFunds{})), + }; + "No Coins Sent to Contract - Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "untrn"), + Coin::new(1_000_000, "osmo"), + ], + fee_swap: None, + user_swap: SwapExactCoinIn { + swap_venue_name: "swap_venue_name".to_string(), + coin_in: None, + operations: vec![ + SwapOperation { + pool: "pool".to_string(), + denom_in: "untrn".to_string(), + denom_out: "osmo".to_string(), + } + ], + }, + min_coin: Coin::new(1_000_000, "osmo"), + timeout_timestamp: 101, + post_swap_action: PostSwapAction::BankSend { + to_address: "to_address".to_string(), + }, + expected_messages: vec![], + expected_error: Some(ContractError::Payment(MultipleDenoms{})), + }; + "More Than One Coin Sent to Contract - Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "untrn"), + ], + fee_swap: None, + user_swap: SwapExactCoinIn { + swap_venue_name: "swap_venue_name".to_string(), + coin_in: None, + operations: vec![], + }, + min_coin: Coin::new(1_000_000, "osmo"), + timeout_timestamp: 101, + post_swap_action: PostSwapAction::BankSend { + to_address: "to_address".to_string(), + }, + expected_messages: vec![], + expected_error: Some(ContractError::UserSwapOperationsEmpty), + }; + "Empty User Swap Operations - Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "osmo"), + ], + fee_swap: Some( + SwapExactCoinOut { + swap_venue_name: "swap_venue_name".to_string(), + coin_out: Coin::new(200_000, "osmo"), + operations: vec![], + } + ), + user_swap: SwapExactCoinIn { + swap_venue_name: "swap_venue_name".to_string(), + coin_in: Some(Coin::new(900_000, "osmo")), + operations: vec![ + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "osmo".to_string(), + denom_out: "uatom".to_string(), + } + ], + }, + min_coin: Coin::new(100_000, "uatom"), + timeout_timestamp: 101, + post_swap_action: PostSwapAction::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "untrn")], + timeout_fee: vec![Coin::new(100_000, "untrn")], + }, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + }, + expected_messages: vec![], + expected_error: Some(ContractError::FeeSwapOperationsEmpty), + }; + "Empty Fee Swap Operations - Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(1_000_000, "untrn"), + ], + fee_swap: None, + user_swap: SwapExactCoinIn { + swap_venue_name: "swap_venue_name".to_string(), + coin_in: None, + operations: vec![], + }, + min_coin: Coin::new(1_000_000, "osmo"), + timeout_timestamp: 99, + post_swap_action: PostSwapAction::BankSend { + to_address: "to_address".to_string(), + }, + expected_messages: vec![], + expected_error: Some(ContractError::Timeout), + }; + "Current Block Time Greater Than Timeout Timestamp - Expect Error")] +fn test_execute_post_swap_action(params: Params) { + // Create mock dependencies + let mut deps = mock_dependencies_with_balances(&[( + "entry_point", + &[Coin::new(1_000_000, "osmo"), Coin::new(1_000_000, "untrn")], + )]); + + // Create mock wasm handler to handle the swap adapter contract query + // Will always return 200_000 osmo + let wasm_handler = |query: &WasmQuery| -> QuerierResult { + match query { + WasmQuery::Smart { .. } => SystemResult::Ok(ContractResult::Ok( + to_binary(&Coin::new(200_000, "osmo")).unwrap(), + )), + _ => panic!("Unsupported query: {:?}", query), + } + }; + + // Update querier with mock wasm handler + deps.querier.update_wasm(wasm_handler); + + // Create mock env with parameters that make testing easier + let mut env = mock_env(); + env.contract.address = Addr::unchecked("entry_point"); + env.block.time = Timestamp::from_nanos(100); + + // Convert info funds vector into a slice of Coin objects + let info_funds: &[Coin] = ¶ms.info_funds; + + // Create mock info with entry point contract address + let info = mock_info("swapper", info_funds); + + // Store the ibc transfer adapter contract address + let swap_venue_adapter = Addr::unchecked("swap_venue_adapter"); + SWAP_VENUE_MAP + .save( + deps.as_mut().storage, + "swap_venue_name", + &swap_venue_adapter, + ) + .unwrap(); + + // Create standardized params used across all tests + // The reason for these params to not need to be defined in each test case is because + // they have no direct impact on the test case + let affiliates: Vec = vec![]; + + // Call execute_swap_and_action with the given test case params + let res = skip_swap_entry_point::contract::execute( + deps.as_mut(), + env, + info, + ExecuteMsg::SwapAndAction { + fee_swap: params.fee_swap, + user_swap: params.user_swap, + min_coin: params.min_coin, + timeout_timestamp: params.timeout_timestamp, + post_swap_action: params.post_swap_action, + affiliates, + }, + ); + + match res { + Ok(res) => { + // Assert the test did not expect an error + assert!( + params.expected_error.is_none(), + "expected test to error with {:?}, but it succeeded", + params.expected_error + ); + + // Assert the number of messages in the response is correct + assert_eq!( + res.messages.len(), + params.expected_messages.len(), + "expected {:?} messages, but got {:?}", + params.expected_messages.len(), + res.messages.len() + ); + + // Assert the messages in the response are correct + assert_eq!(res.messages, params.expected_messages,); + } + Err(err) => { + // Assert the test expected an error + assert!( + params.expected_error.is_some(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err, params.expected_error.unwrap()); + } + } +} diff --git a/contracts/networks/neutron/ibc-transfer/Cargo.toml b/contracts/networks/neutron/ibc-transfer/Cargo.toml new file mode 100644 index 00000000..4384d103 --- /dev/null +++ b/contracts/networks/neutron/ibc-transfer/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "skip-swap-neutron-ibc-transfer" +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-std = { workspace = true } +cw-storage-plus = { workspace = true } +neutron-proto = { workspace = true } +neutron-sdk = { workspace = true } +prost = { workspace = true } +skip = { path = "../../../../packages/skip" } +thiserror = { workspace = true } + +[dev-dependencies] +cosmos-sdk-proto = { workspace = true } +test-case = { workspace = true } \ No newline at end of file diff --git a/contracts/networks/neutron/ibc-transfer/src/contract.rs b/contracts/networks/neutron/ibc-transfer/src/contract.rs new file mode 100644 index 00000000..807bd194 --- /dev/null +++ b/contracts/networks/neutron/ibc-transfer/src/contract.rs @@ -0,0 +1,273 @@ +use crate::{ + error::{ContractError, ContractResult}, + state::{ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER, IN_PROGRESS_IBC_TRANSFER}, +}; +use cosmwasm_std::{ + entry_point, to_binary, BankMsg, Binary, Coin, Deps, DepsMut, Env, MessageInfo, Reply, + Response, SubMsg, SubMsgResult, +}; +use neutron_proto::neutron::transfer::{MsgTransfer, MsgTransferResponse}; +use neutron_sdk::sudo::msg::{RequestPacket, TransferSudoMsg}; +use prost::Message; +use skip::{ + ibc::{ + AckID, ExecuteMsg, IbcInfo, InstantiateMsg, + NeutronInProgressIbcTransfer as InProgressIbcTransfer, NeutronQueryMsg as QueryMsg, + }, + proto_coin::ProtoCoin, + sudo::SudoType, +}; + +const REPLY_ID: u64 = 1; + +/////////////////// +/// INSTANTIATE /// +/////////////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: InstantiateMsg, +) -> ContractResult { + Ok(Response::new().add_attribute("action", "instantiate")) +} + +/////////////// +/// EXECUTE /// +/////////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + _info: MessageInfo, + msg: ExecuteMsg, +) -> ContractResult { + match msg { + ExecuteMsg::IbcTransfer { + info, + coin, + timeout_timestamp, + } => execute_ibc_transfer(deps, env, info, coin, timeout_timestamp), + } +} + +// Converts the given info and coin into a neutron ibc transfer message, +// saves necessary info in case the ibc transfer fails to send funds back to +// a recovery address, and then emits the neutron ibc transfer message as a sub message +fn execute_ibc_transfer( + deps: DepsMut, + env: Env, + info: IbcInfo, + coin: Coin, + timeout_timestamp: u64, +) -> ContractResult { + // Create neutron ibc transfer message + let msg = MsgTransfer { + source_port: "transfer".to_string(), + source_channel: info.source_channel, + token: Some(ProtoCoin(coin.clone()).into()), + sender: env.contract.address.to_string(), + receiver: info.receiver, + timeout_height: None, + timeout_timestamp, + memo: info.memo, + fee: Some(info.fee.clone().into()), + }; + + // Save in progress ibc transfer data (recover address and coin) to storage, to be used in sudo handler + IN_PROGRESS_IBC_TRANSFER.save( + deps.storage, + &InProgressIbcTransfer { + recover_address: info.recover_address, // This address is verified in entry point + coin, + ack_fee: info.fee.ack_fee, + timeout_fee: info.fee.timeout_fee, + }, + )?; + + // Create sub message from neutron ibc transfer message to receive a reply + let sub_msg = SubMsg::reply_on_success(msg, REPLY_ID); + + Ok(Response::new() + .add_submessage(sub_msg) + .add_attribute("action", "execute_ibc_transfer")) +} + +///////////// +/// REPLY /// +///////////// + +// Handles the reply from the neutron ibc transfer sub message +// Upon success, maps the sub msg AckID (channel_id, sequence_id) +// to the in progress ibc transfer struct, and saves it to storage. +// Now that the map entry is stored, it also removes the in progress +// ibc transfer from storage. +#[entry_point] +pub fn reply(deps: DepsMut, _env: Env, reply: Reply) -> ContractResult { + // Error if the reply id is not the same as the one used in the sub message dispatched + // This should never happen since we are using a constant reply id, but added in case + // the wasm module doesn't behave as expected. + if reply.id != REPLY_ID { + unreachable!() + } + + // Get the sub message response from the reply and error if it does not exist + // This should never happen since sub msg was set to reply on success only, + // but added in case the wasm module doesn't behave as expected. + let SubMsgResult::Ok(sub_msg_response) = reply.result else { + unreachable!() + }; + + // Parse the response from the sub message + let resp: MsgTransferResponse = MsgTransferResponse::decode( + sub_msg_response + .data + .ok_or(ContractError::MissingResponseData)? + .as_slice(), + )?; + + // Set ack_id to be the channel id and sequence id from the response as a tuple + let ack_id: AckID = (&resp.channel, resp.sequence_id); + + // Get the in progress ibc transfer from storage + let in_progress_ibc_transfer = IN_PROGRESS_IBC_TRANSFER.load(deps.storage)?; + + // Error if unique ack_id (channel id, sequence id) already exists in storage + if ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER.has(deps.storage, ack_id) { + return Err(ContractError::AckIDAlreadyExists { + channel_id: ack_id.0.into(), + sequence_id: ack_id.1, + }); + } + + // Set the in progress ibc transfer to storage, keyed by channel id and sequence id + ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER.save(deps.storage, ack_id, &in_progress_ibc_transfer)?; + + // Delete the in progress ibc transfer from storage + IN_PROGRESS_IBC_TRANSFER.remove(deps.storage); + + Ok(Response::new().add_attribute("action", "sub_msg_reply_success")) +} + +//////////// +/// SUDO /// +//////////// + +// Handles the sudo acknowledgement from the neutron transfer module upon receiving +// a packet acknowledge form the receiving chain of the ibc transfer +#[entry_point] +pub fn sudo(deps: DepsMut, _env: Env, msg: TransferSudoMsg) -> ContractResult { + // Get request and sudo type from sudo message + let (req, sudo_type) = match msg { + TransferSudoMsg::Response { request, .. } => (request, SudoType::Response), + TransferSudoMsg::Error { request, .. } => (request, SudoType::Error), + TransferSudoMsg::Timeout { request } => (request, SudoType::Timeout), + }; + + // Get ack id (channel id, sequence id) from request packet + let ack_id = get_ack_id(&req)?; + + // Get in progress ibc transfer from storage + let in_progress_ibc_transfer = ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER.load(deps.storage, ack_id)?; + + // Create bank transfer message to send funds back to user's recover address + // based on the sudo type: + // - Response: send the refunded timeout fee back to the user's recover address + // - Error: send the failed ibc transferred coin + refunded timeout fee back to the user's recover address + // - Timeout: send the failed ibc transferred coin + refunded ack fee back to the user's recover address + let amount = match sudo_type { + SudoType::Response => in_progress_ibc_transfer.timeout_fee, + SudoType::Error => { + // Create a single vector of coins to bank send + // that merges the failed ibc transfer coin with + // the timeout fee vector of coins + add_coin_to_vec_coins( + in_progress_ibc_transfer.coin, + in_progress_ibc_transfer.timeout_fee, + )? + } + SudoType::Timeout => { + // Create a single vector of coins to bank send + // that merges the failed ibc transfer coin with + // the ack fee vector of coins + add_coin_to_vec_coins( + in_progress_ibc_transfer.coin, + in_progress_ibc_transfer.ack_fee, + )? + } + }; + + // Create bank send message to send funds back to user's recover address + // This will error if the contract balances are insufficient for the stored + // amount of coins to send (refunded fee + ibc transfer coin if failed). + // This error should never happen since the contract should have enough + // funds if Neutron sudo call works as expected. If it does, the failure + // will be stored in Neutron's ContractManager module, allowing us to query + // and investigate the failure. + let bank_send_msg = BankMsg::Send { + to_address: in_progress_ibc_transfer.recover_address, + amount, + }; + + // Remove ack id <> in progress ibc transfer entry from storage + ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER.remove(deps.storage, ack_id); + + Ok(Response::new() + .add_message(bank_send_msg) + .add_attribute("action", sudo_type)) +} + +//////////////////////// +/// HELPER FUNCTIONS /// +//////////////////////// + +// Helper function that adds a coin to a vector of coins, and returns the vector. +fn add_coin_to_vec_coins(coin_to_add: Coin, mut coins: Vec) -> ContractResult> { + // Iterate through the coins vector and add the coin_to_add to the coin in the vector + match coins + .iter_mut() + .find(|coin| coin.denom == coin_to_add.denom) + { + Some(coin) => { + coin.amount = coin.amount.checked_add(coin_to_add.amount)?; + } + None => { + coins.push(coin_to_add); + } + } + + Ok(coins) +} + +// Helper function to get the ack_id (channel id, sequence id) from a RequestPacket +fn get_ack_id(req: &RequestPacket) -> ContractResult { + // Get the channel id and sequence id from the request packet + let channel_id = req + .source_channel + .as_ref() + .ok_or(ContractError::ChannelIDNotFound)?; + let seq_id = req.sequence.ok_or(ContractError::SequenceNotFound)?; + + // Return the ack_id as a tuple of the channel id and sequence id + Ok((channel_id, seq_id)) +} + +///////////// +/// QUERY /// +///////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> ContractResult { + match msg { + QueryMsg::InProgressIbcTransfer { + channel_id, + sequence_id, + } => to_binary( + &ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER.load(deps.storage, (&channel_id, sequence_id))?, + ), + } + .map_err(From::from) +} diff --git a/contracts/networks/neutron/ibc-transfer/src/error.rs b/contracts/networks/neutron/ibc-transfer/src/error.rs new file mode 100644 index 00000000..43b02713 --- /dev/null +++ b/contracts/networks/neutron/ibc-transfer/src/error.rs @@ -0,0 +1,34 @@ +use cosmwasm_std::{OverflowError, StdError}; +use thiserror::Error; + +pub type ContractResult = core::result::Result; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error("Error decoding Sub Msg Response data to MsgTransferResponse")] + Decode(#[from] prost::DecodeError), + + #[error(transparent)] + Overflow(#[from] OverflowError), + + #[error("Unauthorized")] + Unauthorized, + + #[error("SubMsgResponse does not contain data")] + MissingResponseData, + + #[error("Channel ID not found")] + ChannelIDNotFound, + + #[error("Sequence not found")] + SequenceNotFound, + + #[error("ACK ID already exists for channel ID {channel_id} and sequence ID {sequence_id}")] + AckIDAlreadyExists { + channel_id: String, + sequence_id: u64, + }, +} diff --git a/contracts/networks/neutron/ibc-transfer/src/lib.rs b/contracts/networks/neutron/ibc-transfer/src/lib.rs new file mode 100644 index 00000000..3d3e89c8 --- /dev/null +++ b/contracts/networks/neutron/ibc-transfer/src/lib.rs @@ -0,0 +1,3 @@ +pub mod contract; +pub mod error; +pub mod state; diff --git a/contracts/networks/neutron/ibc-transfer/src/state.rs b/contracts/networks/neutron/ibc-transfer/src/state.rs new file mode 100644 index 00000000..f6566e0e --- /dev/null +++ b/contracts/networks/neutron/ibc-transfer/src/state.rs @@ -0,0 +1,7 @@ +use cw_storage_plus::{Item, Map}; +use skip::ibc::{AckID, NeutronInProgressIbcTransfer as InProgressIbcTransfer}; + +pub const IN_PROGRESS_IBC_TRANSFER: Item = + Item::new("in_progress_ibc_transfer"); +pub const ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER: Map = + Map::new("ack_id_to_transferer"); diff --git a/contracts/networks/neutron/ibc-transfer/tests/test_execute_ibc_transfer.rs b/contracts/networks/neutron/ibc-transfer/tests/test_execute_ibc_transfer.rs new file mode 100644 index 00000000..9a41fe89 --- /dev/null +++ b/contracts/networks/neutron/ibc-transfer/tests/test_execute_ibc_transfer.rs @@ -0,0 +1,142 @@ +use cosmos_sdk_proto::cosmos::base::v1beta1::Coin as CosmosSdkCoin; +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env, mock_info}, + Addr, Coin, + ReplyOn::Success, + SubMsg, Uint128, +}; +use neutron_proto::neutron::{feerefunder::Fee as NeutronFee, transfer::MsgTransfer}; +use skip::ibc::{ + ExecuteMsg, IbcFee, IbcInfo, NeutronInProgressIbcTransfer as InProgressIBCTransfer, +}; +use skip_swap_neutron_ibc_transfer::{error::ContractResult, state::IN_PROGRESS_IBC_TRANSFER}; +use test_case::test_case; + +/* +Test Cases: + +Expect Response + - Happy Path (tests the message emitted is expected and the in progress ibc transfer is saved correctly) + +// No expected error cases since this function mainly does +// type-safe conversions and the only error cases are if the +// contract doesn't: +// 1. have enough balance to execute the ibc transfer, +// 2. timeout_timestamp has passed already on the dest chain via ibc-go module checking, +// 3. generate a valid packet data for the dest chain via ibc-go module checking, +// Which all require running a simulation app env to test, and not unit tests. + */ + +// Define test parameters +struct Params { + ibc_adapter_contract_address: Addr, + coin: Coin, + ibc_info: IbcInfo, + timeout_timestamp: u64, + expected_messages: Vec, + expected_in_progress_ibc_transfer: InProgressIBCTransfer, +} + +// Test execute_ibc_transfer +#[test_case( + Params { + ibc_adapter_contract_address: Addr::unchecked("ibc_transfer".to_string()), + coin: Coin::new(100, "osmo"), + ibc_info: IbcInfo { + source_channel: "source_channel".to_string(), + receiver: "receiver".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin { + denom: "ntrn".to_string(), + amount: Uint128::new(10), + }], + timeout_fee: vec![], + }, + memo: "memo".to_string(), + recover_address: "recover_address".to_string(), + }, + timeout_timestamp: 100, + expected_messages: vec![SubMsg { + id: 1, + msg: MsgTransfer { + source_port: "transfer".to_string(), + source_channel: "source_channel".to_string(), + token: Some(CosmosSdkCoin { + denom: "osmo".to_string(), + amount: "100".to_string(), + }), + sender: "ibc_transfer".to_string(), + receiver: "receiver".to_string(), + timeout_height: None, + timeout_timestamp: 100, + memo: "memo".to_string(), + fee: Some(NeutronFee { + recv_fee: vec![], + ack_fee: vec![CosmosSdkCoin { + denom: "ntrn".to_string(), + amount: "10".to_string(), + }], + timeout_fee: vec![], + }), + } + .into(), + gas_limit: None, + reply_on: Success, + }], + expected_in_progress_ibc_transfer: InProgressIBCTransfer { + recover_address: "recover_address".to_string(), + coin: Coin::new(100, "osmo"), + ack_fee: vec![Coin { + denom: "ntrn".to_string(), + amount: Uint128::new(10), + }], + timeout_fee: vec![], + }, + }; + "Happy Path")] +fn test_execute_ibc_transfer(params: Params) -> ContractResult<()> { + // Create mock dependencies + let mut deps = mock_dependencies(); + + // Create mock env + let mut env = mock_env(); + env.contract.address = params.ibc_adapter_contract_address.clone(); + + // Create mock info + let info = mock_info("caller", &[]); + + // Call execute_ibc_transfer with the given test parameters + let res = skip_swap_neutron_ibc_transfer::contract::execute( + deps.as_mut(), + env, + info, + ExecuteMsg::IbcTransfer { + info: params.ibc_info.clone(), + coin: params.coin.clone(), + timeout_timestamp: params.timeout_timestamp, + }, + ); + + // Assert the behavior is correct + match res { + Ok(res) => { + // Assert the messages in the response are correct + assert_eq!(res.messages, params.expected_messages); + + // Load the in progress ibc transfer from state and verify it is correct + let stored_in_progress_ibc_transfer = IN_PROGRESS_IBC_TRANSFER.load(&deps.storage)?; + + // Assert the in progress ibc transfer is correct + assert_eq!( + stored_in_progress_ibc_transfer, + params.expected_in_progress_ibc_transfer + ); + } + Err(err) => { + panic!("unexpected error: {:?}", err) + } + } + + Ok(()) +} diff --git a/contracts/networks/neutron/ibc-transfer/tests/test_reply.rs b/contracts/networks/neutron/ibc-transfer/tests/test_reply.rs new file mode 100644 index 00000000..e487317e --- /dev/null +++ b/contracts/networks/neutron/ibc-transfer/tests/test_reply.rs @@ -0,0 +1,263 @@ +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env}, + Coin, Reply, StdError, SubMsgResponse, SubMsgResult, Uint128, +}; +use neutron_proto::neutron::transfer::MsgTransferResponse; +use prost::Message; +use skip::ibc::NeutronInProgressIbcTransfer as InProgressIBCTransfer; +use skip_swap_neutron_ibc_transfer::{ + error::{ContractError, ContractResult}, + state::{ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER, IN_PROGRESS_IBC_TRANSFER}, +}; +use test_case::test_case; + +/* +Test Cases: + +Expect Success + - Happy Path (tests the in progress ibc transfer is removed from storage and the ack id to in progress ibc transfer map entry is correct) + +Expect Error + - Missing Sub Msg Response Data + - Invalid Sub Msg Response Data To Convert To MsgTransferResponse + - No In Progress Ibc Transfer To Load + - Ack ID Already Exists + - SubMsg Incorrect Reply ID + +Expect Panic + - SubMsgResult Error + - Should panic because the sub msg is set to reply only on success, so should never happen + unless the wasm module worked unexpectedly + - SubMsg Incorrect Reply ID + - Should panic because the reply id is set to a constant, so should never happen unless + the wasm module worked unexpectedly + */ + +// Define test parameters +struct Params { + channel_id: String, + sequence_id: u64, + reply: Reply, + pre_reply_in_progress_ibc_transfer: Option, + store_ack_id_to_in_progress_ibc_transfer: bool, + expected_error: Option, +} + +// Test reply +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 5, + reply: Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(MsgTransferResponse {sequence_id: 5, channel: "channel_id".to_string() }.encode_to_vec().as_slice().into()), + }), + }, + pre_reply_in_progress_ibc_transfer: Some(InProgressIBCTransfer { + recover_address: "recover_address".to_string(), + coin: Coin::new(100, "osmo"), + ack_fee: vec![Coin { + denom: "ntrn".to_string(), + amount: Uint128::new(10), + }], + timeout_fee: vec![Coin { + denom: "ntrn".to_string(), + amount: Uint128::new(10), + }] + }), + store_ack_id_to_in_progress_ibc_transfer: false, + expected_error: None, + }; + "Happy Path")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + reply: Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: None, + }), + }, + pre_reply_in_progress_ibc_transfer: None, + store_ack_id_to_in_progress_ibc_transfer: false, + expected_error: Some(ContractError::MissingResponseData), + }; + "Missing Sub Msg Response Data - Expect Error")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + reply: Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(b"invalid".into()), + }), + }, + pre_reply_in_progress_ibc_transfer: None, + store_ack_id_to_in_progress_ibc_transfer: false, + expected_error: Some(ContractError::Decode(prost::DecodeError::new("buffer underflow".to_string()))), + }; + "Invalid Sub Msg Response Data To Convert To MsgTransferResponse - Expect Error")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + reply: Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(MsgTransferResponse {sequence_id: 1, channel: "channel_id".to_string() }.encode_to_vec().as_slice().into()), + }), + }, + pre_reply_in_progress_ibc_transfer: None, + store_ack_id_to_in_progress_ibc_transfer: false, + expected_error: Some(ContractError::Std(StdError::NotFound { kind: "skip::ibc::NeutronInProgressIbcTransfer".to_string() })), + }; + "No In Progress Ibc Transfer To Load - Expect Error")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 5, + reply: Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(MsgTransferResponse {sequence_id: 5, channel: "channel_id".to_string() }.encode_to_vec().as_slice().into()), + }), + }, + pre_reply_in_progress_ibc_transfer: Some(InProgressIBCTransfer { + recover_address: "recover_address".to_string(), + coin: Coin::new(100, "osmo"), + ack_fee: vec![Coin { + denom: "ntrn".to_string(), + amount: Uint128::new(10), + }], + timeout_fee: vec![Coin { + denom: "ntrn".to_string(), + amount: Uint128::new(10), + }] + }), + store_ack_id_to_in_progress_ibc_transfer: true, + expected_error: Some(ContractError::AckIDAlreadyExists { channel_id: "channel_id".to_string(), sequence_id: 5 }), + }; + "Ack ID Already Exists - Expect Error")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + reply: Reply { + id: 2, + result: SubMsgResult::Err("".to_string()), + }, + pre_reply_in_progress_ibc_transfer: Some(InProgressIBCTransfer { + recover_address: "recover_address".to_string(), + coin: Coin::new(100, "osmo"), + ack_fee: vec![], + timeout_fee: vec![] + }), + store_ack_id_to_in_progress_ibc_transfer: false, + expected_error: None, + } => panics "internal error: entered unreachable code"; + "SubMsg Incorrect Reply ID - Expect Panic")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + reply: Reply { + id: 1, + result: SubMsgResult::Err("".to_string()), + }, + pre_reply_in_progress_ibc_transfer: Some(InProgressIBCTransfer { + recover_address: "recover_address".to_string(), + coin: Coin::new(100, "osmo"), + ack_fee: vec![Coin { + denom: "ntrn".to_string(), + amount: Uint128::new(10), + }], + timeout_fee: vec![Coin { + denom: "ntrn".to_string(), + amount: Uint128::new(10), + }] + }), + expected_error: None, + store_ack_id_to_in_progress_ibc_transfer: false, + } => panics "internal error: entered unreachable code"; + "SubMsgResult Error - Expect Panic")] +fn test_reply(params: Params) -> ContractResult<()> { + // Create mock dependencies + let mut deps = mock_dependencies(); + + // Create mock env + let env = mock_env(); + + // Store the in progress ibc transfer to state if it exists + if let Some(in_progress_ibc_transfer) = params.pre_reply_in_progress_ibc_transfer.clone() { + IN_PROGRESS_IBC_TRANSFER.save(deps.as_mut().storage, &in_progress_ibc_transfer)?; + } + + // If the test expects the ack id to in progress ibc transfer map entry to be stored, + // store it to state + if params.store_ack_id_to_in_progress_ibc_transfer { + ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER.save( + deps.as_mut().storage, + (¶ms.channel_id, params.sequence_id), + ¶ms.pre_reply_in_progress_ibc_transfer.clone().unwrap(), + )?; + } + + // Call reply with the given test parameters + let res = skip_swap_neutron_ibc_transfer::contract::reply(deps.as_mut(), env, params.reply); + + // Assert the behavior is correct + match res { + Ok(_) => { + // Assert the test did not expect an error + assert!( + params.expected_error.is_none(), + "expected test to error with {:?}, but it succeeded", + params.expected_error + ); + + // Verify the in progress ibc transfer was removed from storage + match IN_PROGRESS_IBC_TRANSFER.load(&deps.storage) { + Ok(in_progress_ibc_transfer) => { + panic!( + "expected in progress ibc transfer to be removed: {:?}", + in_progress_ibc_transfer + ) + } + Err(err) => assert_eq!( + err, + StdError::NotFound { + kind: "skip::ibc::NeutronInProgressIbcTransfer".to_string() + } + ), + }; + + // Verify the stored ack id to in progress ibc transfer map entry is correct + assert_eq!( + ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER + .load(&deps.storage, (¶ms.channel_id, params.sequence_id))?, + params.pre_reply_in_progress_ibc_transfer.unwrap() + ); + } + Err(err) => { + // Assert the test expected an error + assert!( + params.expected_error.is_some(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err, params.expected_error.unwrap()); + } + } + + Ok(()) +} diff --git a/contracts/networks/neutron/ibc-transfer/tests/test_sudo.rs b/contracts/networks/neutron/ibc-transfer/tests/test_sudo.rs new file mode 100644 index 00000000..d4a8056c --- /dev/null +++ b/contracts/networks/neutron/ibc-transfer/tests/test_sudo.rs @@ -0,0 +1,368 @@ +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env}, + BankMsg, Binary, Coin, + ReplyOn::Never, + StdError, SubMsg, +}; +use neutron_sdk::sudo::msg::{RequestPacket, TransferSudoMsg}; +use skip::ibc::NeutronInProgressIbcTransfer as InProgressIBCTransfer; +use skip_swap_neutron_ibc_transfer::{ + error::{ContractError, ContractResult}, + state::ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER, +}; +use test_case::test_case; + +/* +Test Cases: + +Expect Success + - Sudo Response - Happy Path - Send Timeout Fee + - Sudo Timeout - Send Ibc Coin And Ack Fee Same Denom + - Sudo Timeout - Send Ibc Coin And Ack Fee Different Denom + - Sudo Error - Send Ibc Coin And Timeout Fee Same Denom + - Sudo Error - Send Ibc Coin And Timeout Fee Different Denom + +Expect Error + - No In Progress Ibc Transfer Mapped To Sudo Ack ID - Expect Error + - No channel id in TransferSudoMsg - Expect Error + - No sequence in TransferSudoMsg - Expect Error + + */ + +// Define test parameters +struct Params { + channel_id: String, + sequence_id: u64, + sudo_msg: TransferSudoMsg, + stored_in_progress_ibc_transfer: Option, + expected_messages: Vec, + expected_error: Option, +} + +// Test sudo +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + sudo_msg: TransferSudoMsg::Response { + request: RequestPacket { + sequence: Some(1), + source_port: None, + source_channel: Some("channel_id".to_string()), + destination_port: None, + destination_channel: None, + data: None, + timeout_height: None, + timeout_timestamp: None, + }, + data: Binary::from(b""), + }, + stored_in_progress_ibc_transfer: Some(InProgressIBCTransfer { + recover_address: "recover_address".to_string(), + coin: Coin::new(100, "osmo"), + ack_fee: vec![Coin::new(10, "osmo")], + timeout_fee: vec![Coin::new(20, "osmo")], + }), + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "recover_address".to_string(), + amount: vec![Coin::new(20, "osmo")], + }.into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Sudo Response - Happy Path - Send Timeout Fee")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + sudo_msg: TransferSudoMsg::Timeout { + request: RequestPacket { + sequence: Some(1), + source_port: None, + source_channel: Some("channel_id".to_string()), + destination_port: None, + destination_channel: None, + data: None, + timeout_height: None, + timeout_timestamp: None, + }, + }, + stored_in_progress_ibc_transfer: Some(InProgressIBCTransfer { + recover_address: "recover_address".to_string(), + coin: Coin::new(100, "osmo"), + ack_fee: vec![Coin::new(10, "osmo")], + timeout_fee: vec![Coin::new(20, "osmo")], + }), + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "recover_address".to_string(), + amount: vec![Coin::new(110, "osmo")], + }.into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Sudo Timeout - Send Ibc Coin And Ack Fee Same Denom")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + sudo_msg: TransferSudoMsg::Timeout { + request: RequestPacket { + sequence: Some(1), + source_port: None, + source_channel: Some("channel_id".to_string()), + destination_port: None, + destination_channel: None, + data: None, + timeout_height: None, + timeout_timestamp: None, + }, + }, + stored_in_progress_ibc_transfer: Some(InProgressIBCTransfer { + recover_address: "recover_address".to_string(), + coin: Coin::new(100, "osmo"), + ack_fee: vec![Coin::new(10, "ntrn")], + timeout_fee: vec![Coin::new(10, "osmo")], + }), + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "recover_address".to_string(), + amount: vec![ Coin::new(10, "ntrn"), Coin::new(100, "osmo")], + }.into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Sudo Timeout - Send Ibc Coin And Ack Fee Different Denom")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + sudo_msg: TransferSudoMsg::Error { + request: RequestPacket { + sequence: Some(1), + source_port: None, + source_channel: Some("channel_id".to_string()), + destination_port: None, + destination_channel: None, + data: None, + timeout_height: None, + timeout_timestamp: None, + }, + details: "".to_string(), + }, + stored_in_progress_ibc_transfer: Some(InProgressIBCTransfer { + recover_address: "recover_address".to_string(), + coin: Coin::new(100, "osmo"), + ack_fee: vec![Coin::new(20, "osmo")], + timeout_fee: vec![Coin::new(10, "osmo")], + }), + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "recover_address".to_string(), + amount: vec![Coin::new(110, "osmo")], + }.into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Sudo Error - Send Ibc Coin And Timeout Fee Same Denom")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + sudo_msg: TransferSudoMsg::Error { + request: RequestPacket { + sequence: Some(1), + source_port: None, + source_channel: Some("channel_id".to_string()), + destination_port: None, + destination_channel: None, + data: None, + timeout_height: None, + timeout_timestamp: None, + }, + details: "".to_string(), + }, + stored_in_progress_ibc_transfer: Some(InProgressIBCTransfer { + recover_address: "recover_address".to_string(), + coin: Coin::new(100, "osmo"), + ack_fee: vec![Coin::new(20, "osmo")], + timeout_fee: vec![Coin::new(10, "ntrn")], + }), + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "recover_address".to_string(), + amount: vec![ Coin::new(10, "ntrn"), Coin::new(100, "osmo")], + }.into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Sudo Error - Send Ibc Coin And Timeout Fee Different Denom")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + sudo_msg: TransferSudoMsg::Error { + request: RequestPacket { + sequence: Some(1), + source_port: None, + source_channel: Some("channel_id".to_string()), + destination_port: None, + destination_channel: None, + data: None, + timeout_height: None, + timeout_timestamp: None, + }, + details: "".to_string(), + }, + stored_in_progress_ibc_transfer: None, + expected_messages: vec![], + expected_error: Some(ContractError::Std(StdError::NotFound { + kind: "skip::ibc::NeutronInProgressIbcTransfer".to_string(), + })), + }; + "No In Progress Ibc Transfer Mapped To Sudo Ack ID - Expect Error")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + sudo_msg: TransferSudoMsg::Error { + request: RequestPacket { + sequence: Some(1), + source_port: None, + source_channel: None, + destination_port: None, + destination_channel: None, + data: None, + timeout_height: None, + timeout_timestamp: None, + }, + details: "".to_string(), + }, + stored_in_progress_ibc_transfer: None, + expected_messages: vec![], + expected_error: Some(ContractError::ChannelIDNotFound), + }; + "No channel id in TransferSudoMsg - Expect Error")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + sudo_msg: TransferSudoMsg::Error { + request: RequestPacket { + sequence: None, + source_port: None, + source_channel: Some("channel_id".to_string()), + destination_port: None, + destination_channel: None, + data: None, + timeout_height: None, + timeout_timestamp: None, + }, + details: "".to_string(), + }, + stored_in_progress_ibc_transfer: None, + expected_messages: vec![], + expected_error: Some(ContractError::SequenceNotFound), + }; + "No sequence in TransferSudoMsg - Expect Error")] +fn test_sudo(params: Params) -> ContractResult<()> { + // Create mock dependencies + let mut deps = mock_dependencies(); + + // Create mock env + let env = mock_env(); + + // Store the in progress ibc transfer to state if it exists + if let Some(in_progress_ibc_transfer) = params.stored_in_progress_ibc_transfer.clone() { + ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER.save( + deps.as_mut().storage, + (¶ms.channel_id, params.sequence_id), + &in_progress_ibc_transfer, + )?; + } + + // Call sudo with the given test parameters + let res = skip_swap_neutron_ibc_transfer::contract::sudo(deps.as_mut(), env, params.sudo_msg); + + // Assert the behavior is correct + match res { + Ok(res) => { + // Assert the test did not expect an error + assert!( + params.expected_error.is_none(), + "expected test to error with {:?}, but it succeeded", + params.expected_error + ); + + // Verify the in progress ibc transfer was removed from storage + match ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER + .load(&deps.storage, (¶ms.channel_id, params.sequence_id)) + { + Ok(in_progress_ibc_transfer) => { + panic!( + "expected in progress ibc transfer to be removed: {:?}", + in_progress_ibc_transfer + ) + } + Err(err) => assert_eq!( + err, + StdError::NotFound { + kind: "skip::ibc::NeutronInProgressIbcTransfer".to_string() + } + ), + }; + + // Verify the messages in the response are correct + assert_eq!(res.messages, params.expected_messages); + } + Err(err) => { + println!("Here"); + // Assert the test expected an error + assert!( + params.expected_error.is_some(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err, params.expected_error.unwrap()); + + if params.stored_in_progress_ibc_transfer.is_some() { + // Verify the ack id to in progress ibc transfer map entry is still stored + assert_eq!( + ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER + .load(&deps.storage, (¶ms.channel_id, params.sequence_id))?, + params.stored_in_progress_ibc_transfer.unwrap() + ); + } + } + } + + Ok(()) +} diff --git a/contracts/networks/neutron/swap/Cargo.toml b/contracts/networks/neutron/swap/Cargo.toml new file mode 100644 index 00000000..5226f1fa --- /dev/null +++ b/contracts/networks/neutron/swap/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "skip-swap-neutron-astroport-swap" +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 } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +skip = { path = "../../../../packages/skip" } +thiserror = { workspace = true } + +[dev-dependencies] +test-case = { workspace = true } \ No newline at end of file diff --git a/contracts/networks/neutron/swap/src/contract.rs b/contracts/networks/neutron/swap/src/contract.rs new file mode 100644 index 00000000..c968cc8d --- /dev/null +++ b/contracts/networks/neutron/swap/src/contract.rs @@ -0,0 +1,251 @@ +use crate::{ + error::{ContractError, ContractResult}, + state::ROUTER_CONTRACT_ADDRESS, +}; +use astroport::{ + asset::{Asset, AssetInfo}, + pair::{QueryMsg as PairQueryMsg, ReverseSimulationResponse}, + router::{ + ExecuteMsg as RouterExecuteMsg, QueryMsg as RouterQueryMsg, SimulateSwapOperationsResponse, + }, +}; +use cosmwasm_std::{ + entry_point, to_binary, Addr, BankMsg, Binary, Coin, Deps, DepsMut, Env, MessageInfo, Response, + Uint128, WasmMsg, +}; +use cw_utils::one_coin; +use skip::swap::{ExecuteMsg, NeutronInstantiateMsg as InstantiateMsg, QueryMsg, SwapOperation}; + +/////////////////// +/// INSTANTIATE /// +/////////////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> ContractResult { + // Validate router contract address + let checked_router_contract_address = deps.api.addr_validate(&msg.router_contract_address)?; + + // Store the router contract address + ROUTER_CONTRACT_ADDRESS.save(deps.storage, &checked_router_contract_address)?; + + Ok(Response::new() + .add_attribute("action", "instantiate") + .add_attribute( + "router_contract_address", + checked_router_contract_address.to_string(), + )) +} + +/////////////// +/// EXECUTE /// +/////////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> ContractResult { + match msg { + ExecuteMsg::Swap { operations } => execute_swap(deps, env, info, operations), + ExecuteMsg::TransferFundsBack { caller } => execute_transfer_funds_back(deps, env, caller), + } +} + +fn execute_swap( + deps: DepsMut, + env: Env, + info: MessageInfo, + operations: Vec, +) -> ContractResult { + // Get coin in from the message info, error if there is not exactly one coin sent + let coin_in = one_coin(&info)?; + + // Create the astroport swap message + let swap_msg = create_astroport_swap_msg( + ROUTER_CONTRACT_ADDRESS.load(deps.storage)?, + coin_in, + operations, + )?; + + // Create the transfer funds back message + let transfer_funds_back_msg = WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_binary(&ExecuteMsg::TransferFundsBack { + caller: info.sender, + })?, + funds: vec![], + }; + + Ok(Response::new() + .add_message(swap_msg) + .add_message(transfer_funds_back_msg) + .add_attribute("action", "dispatch_swap_and_transfer_back")) +} + +// Query the contract's balance and transfer the funds back to the caller +fn execute_transfer_funds_back(deps: DepsMut, env: Env, caller: Addr) -> ContractResult { + // Create the bank message send to transfer the contract funds back to the caller + let transfer_funds_back_msg = BankMsg::Send { + to_address: caller.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")) +} + +//////////////////////// +/// HELPER FUNCTIONS /// +//////////////////////// + +// Converts the swap operations to astroport AstroSwap operations +fn create_astroport_swap_msg( + router_contract_address: Addr, + coin_in: Coin, + swap_operations: Vec, +) -> ContractResult { + // Convert the swap operations to astroport swap operations + let astroport_swap_operations = swap_operations.into_iter().map(From::from).collect(); + + // Create the astroport router execute message arguments + let astroport_router_msg_args = RouterExecuteMsg::ExecuteSwapOperations { + operations: astroport_swap_operations, + minimum_receive: None, + to: None, + max_spread: None, + }; + + // Create the astroport router swap message + let swap_msg = WasmMsg::Execute { + contract_addr: router_contract_address.to_string(), + msg: to_binary(&astroport_router_msg_args)?, + funds: vec![coin_in], + }; + + Ok(swap_msg) +} + +///////////// +/// QUERY /// +///////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> ContractResult { + match msg { + QueryMsg::RouterContractAddress {} => { + to_binary(&ROUTER_CONTRACT_ADDRESS.load(deps.storage)?) + } + QueryMsg::SimulateSwapExactCoinIn { + coin_in, + swap_operations, + } => to_binary(&query_simulate_swap_exact_coin_in( + deps, + coin_in, + swap_operations, + )?), + QueryMsg::SimulateSwapExactCoinOut { + coin_out, + swap_operations, + } => to_binary(&query_simulate_swap_exact_coin_out( + deps, + coin_out, + swap_operations, + )?), + } + .map_err(From::from) +} + +// Queries the astroport router contract to simulate a swap exact amount in +fn query_simulate_swap_exact_coin_in( + deps: Deps, + coin_in: Coin, + swap_operations: Vec, +) -> ContractResult { + // Error if swap operations is empty + let (Some(first_op), Some(last_op)) = (swap_operations.first(), swap_operations.last()) else { + return Err(ContractError::SwapOperationsEmpty); + }; + + // Ensure coin_in's denom is the same as the first swap operation's denom in + if coin_in.denom != first_op.denom_in { + return Err(ContractError::CoinInDenomMismatch); + } + + // Get the router contract address + let router_contract_address = ROUTER_CONTRACT_ADDRESS.load(deps.storage)?; + + // Get denom out from last swap operation + let denom_out = last_op.denom_out.clone(); + + // Convert the swap operations to astroport swap operations + let astroport_swap_operations = swap_operations.into_iter().map(From::from).collect(); + + // Query the astroport router contract to simulate the swap operations + let res: SimulateSwapOperationsResponse = deps.querier.query_wasm_smart( + router_contract_address, + &RouterQueryMsg::SimulateSwapOperations { + offer_amount: coin_in.amount, + operations: astroport_swap_operations, + }, + )?; + + // Return the coin out + Ok(Coin { + denom: denom_out, + amount: res.amount, + }) +} + +// Queries the astroport pool contracts to simulate a multi-hop swap exact amount out +fn query_simulate_swap_exact_coin_out( + deps: Deps, + coin_out: Coin, + swap_operations: Vec, +) -> ContractResult { + // Error if swap operations is empty + let Some(last_op) = swap_operations.last() else { + return Err(ContractError::SwapOperationsEmpty); + }; + + // Ensure coin_out's denom is the same as the last swap operation's denom out + if coin_out.denom != last_op.denom_out { + return Err(ContractError::CoinOutDenomMismatch); + } + + // Iterate through the swap operations in reverse order, querying the astroport pool contracts + // contracts to get the coin in needed for each swap operation, and then updating the coin in + // needed for the next swap operation until the coin in needed for the first swap operation is found. + let coin_in_needed = swap_operations.iter().rev().try_fold( + coin_out, + |coin_in_needed, operation| -> Result<_, ContractError> { + let res: ReverseSimulationResponse = deps.querier.query_wasm_smart( + &operation.pool, + &PairQueryMsg::ReverseSimulation { + offer_asset_info: None, + ask_asset: Asset { + info: AssetInfo::NativeToken { + denom: coin_in_needed.denom, + }, + amount: coin_in_needed.amount, + }, + }, + )?; + + Ok(Coin { + denom: operation.denom_in.clone(), + amount: res.offer_amount.checked_add(Uint128::one())?, + }) + }, + )?; + + // Return the coin in needed + Ok(coin_in_needed) +} diff --git a/contracts/networks/neutron/swap/src/error.rs b/contracts/networks/neutron/swap/src/error.rs new file mode 100644 index 00000000..18d46231 --- /dev/null +++ b/contracts/networks/neutron/swap/src/error.rs @@ -0,0 +1,25 @@ +use cosmwasm_std::{OverflowError, StdError}; +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)] + Payment(#[from] cw_utils::PaymentError), + + #[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, +} diff --git a/contracts/networks/neutron/swap/src/lib.rs b/contracts/networks/neutron/swap/src/lib.rs new file mode 100644 index 00000000..3d3e89c8 --- /dev/null +++ b/contracts/networks/neutron/swap/src/lib.rs @@ -0,0 +1,3 @@ +pub mod contract; +pub mod error; +pub mod state; diff --git a/contracts/networks/neutron/swap/src/state.rs b/contracts/networks/neutron/swap/src/state.rs new file mode 100644 index 00000000..4a71ee61 --- /dev/null +++ b/contracts/networks/neutron/swap/src/state.rs @@ -0,0 +1,4 @@ +use cosmwasm_std::Addr; +use cw_storage_plus::Item; + +pub const ROUTER_CONTRACT_ADDRESS: Item = Item::new("router_contract_address"); diff --git a/contracts/networks/neutron/swap/tests/test_execute_swap.rs b/contracts/networks/neutron/swap/tests/test_execute_swap.rs new file mode 100644 index 00000000..f2b976fa --- /dev/null +++ b/contracts/networks/neutron/swap/tests/test_execute_swap.rs @@ -0,0 +1,271 @@ +use astroport::{ + asset::AssetInfo, + router::{ExecuteMsg as RouterExecuteMsg, SwapOperation as AstroportSwapOperation}, +}; +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env, mock_info}, + to_binary, Addr, Coin, + ReplyOn::Never, + SubMsg, WasmMsg, +}; +use skip::swap::{ExecuteMsg, SwapOperation}; +use skip_swap_neutron_astroport_swap::{ + error::{ContractError, ContractResult}, + state::ROUTER_CONTRACT_ADDRESS, +}; +use test_case::test_case; + +/* +Test Cases: + +Expect Success + - One Swap Operation + - Multiple Swap Operations + - No Swap Operations (This is prevented in the entry point contract; and will fail on Astroport router if attempted) + +Expect Error + - No Coin Sent + - More Than One Coin Sent + + */ + +// Define test parameters +struct Params { + info_funds: Vec, + swap_operations: Vec, + expected_messages: Vec, + expected_error: Option, +} + +// Test execute_swap +#[test_case( + Params { + info_funds: vec![Coin::new(100, "uosmo")], + swap_operations: vec![ + SwapOperation { + pool: "pool_1".to_string(), + denom_in: "uosmo".to_string(), + denom_out: "uatom".to_string(), + } + ], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "router_contract".to_string(), + msg: to_binary(&RouterExecuteMsg::ExecuteSwapOperations { + operations: vec![ + AstroportSwapOperation::AstroSwap { + offer_asset_info: AssetInfo::NativeToken { + denom: "uosmo".to_string(), + }, + ask_asset_info: AssetInfo::NativeToken { + denom: "uatom".to_string(), + }, + } + ], + minimum_receive: None, + to: None, + max_spread: None, + })?, + funds: vec![Coin::new(100, "uosmo")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_contract_address".to_string(), + msg: to_binary(&ExecuteMsg::TransferFundsBack { + caller: Addr::unchecked("swapper"), + })?, + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "One Swap Operation")] +#[test_case( + Params { + info_funds: vec![Coin::new(100, "uosmo")], + swap_operations: vec![ + SwapOperation { + pool: "pool_1".to_string(), + denom_in: "uosmo".to_string(), + denom_out: "uatom".to_string(), + }, + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "uatom".to_string(), + denom_out: "untrn".to_string(), + } + ], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "router_contract".to_string(), + msg: to_binary(&RouterExecuteMsg::ExecuteSwapOperations { + operations: vec![ + AstroportSwapOperation::AstroSwap { + offer_asset_info: AssetInfo::NativeToken { + denom: "uosmo".to_string(), + }, + ask_asset_info: AssetInfo::NativeToken { + denom: "uatom".to_string(), + }, + }, + AstroportSwapOperation::AstroSwap { + offer_asset_info: AssetInfo::NativeToken { + denom: "uatom".to_string(), + }, + ask_asset_info: AssetInfo::NativeToken { + denom: "untrn".to_string(), + }, + } + ], + minimum_receive: None, + to: None, + max_spread: None, + })?, + funds: vec![Coin::new(100, "uosmo")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_contract_address".to_string(), + msg: to_binary(&ExecuteMsg::TransferFundsBack { + caller: Addr::unchecked("swapper"), + })?, + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Multiple Swap Operations")] +#[test_case( + Params { + info_funds: vec![Coin::new(100, "uosmo")], + swap_operations: vec![], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "router_contract".to_string(), + msg: to_binary(&RouterExecuteMsg::ExecuteSwapOperations { + operations: vec![], + minimum_receive: None, + to: None, + max_spread: None, + })?, + funds: vec![Coin::new(100, "uosmo")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_contract_address".to_string(), + msg: to_binary(&ExecuteMsg::TransferFundsBack { + caller: Addr::unchecked("swapper"), + })?, + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "No Swap Operations")] +#[test_case( + Params { + info_funds: vec![], + swap_operations: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Payment(cw_utils::PaymentError::NoFunds{})), + }; + "No Coin Sent - Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(100, "untrn"), + Coin::new(100, "uosmo"), + ], + swap_operations: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::Payment(cw_utils::PaymentError::MultipleDenoms{})), + }; + "More Than One Coin Sent - Expect Error")] +fn test_execute_swap(params: Params) -> ContractResult<()> { + // Create mock dependencies + let mut deps = mock_dependencies(); + + // Create mock env + let mut env = mock_env(); + env.contract.address = Addr::unchecked("swap_contract_address"); + + // Convert info funds vector into a slice of Coin objects + let info_funds: &[Coin] = ¶ms.info_funds; + + // Create mock info with entry point contract address + let info = mock_info("swapper", info_funds); + + // Store the router contract address + ROUTER_CONTRACT_ADDRESS.save(deps.as_mut().storage, &Addr::unchecked("router_contract"))?; + + // Call execute_swap with the given test parameters + let res = skip_swap_neutron_astroport_swap::contract::execute( + deps.as_mut(), + env, + info, + ExecuteMsg::Swap { + operations: params.swap_operations.clone(), + }, + ); + + // Assert the behavior is correct + match res { + Ok(res) => { + // Assert the test did not expect an error + assert!( + params.expected_error.is_none(), + "expected test to error with {:?}, but it succeeded", + params.expected_error + ); + + // Assert the messages are correct + assert_eq!(res.messages, params.expected_messages); + } + Err(err) => { + // Assert the test expected an error + assert!( + params.expected_error.is_some(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err, params.expected_error.unwrap()); + } + } + + Ok(()) +} diff --git a/contracts/networks/neutron/swap/tests/test_execute_transfer_funds_back.rs b/contracts/networks/neutron/swap/tests/test_execute_transfer_funds_back.rs new file mode 100644 index 00000000..d2b5a78e --- /dev/null +++ b/contracts/networks/neutron/swap/tests/test_execute_transfer_funds_back.rs @@ -0,0 +1,136 @@ +use cosmwasm_std::{ + testing::{mock_dependencies_with_balances, mock_env, mock_info}, + Addr, BankMsg, Coin, + ReplyOn::Never, + SubMsg, +}; +use skip::swap::ExecuteMsg; +use skip_swap_neutron_astroport_swap::error::{ContractError, ContractResult}; +use test_case::test_case; + +/* +Test Cases: + +Expect Success + - One Coin Balance + - Multiple Coin Balance + - No Coin Balance (This will fail at the bank module if attempted) + */ + +// Define test parameters +struct Params { + contract_balance: Vec, + expected_messages: Vec, + expected_error: Option, +} + +// Test execute_swap +#[test_case( + Params { + contract_balance: vec![Coin::new(100, "uosmo")], + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "swapper".to_string(), + amount: vec![Coin::new(100, "uosmo")], + }.into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Transfers One Coin Balance")] +#[test_case( + Params { + contract_balance: vec![ + Coin::new(100, "uosmo"), + Coin::new(100, "uatom"), + ], + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "swapper".to_string(), + amount: vec![ + Coin::new(100, "uosmo"), + Coin::new(100, "uatom") + ], + }.into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Transfers Multiple Coin Balance")] +#[test_case( + Params { + contract_balance: vec![], + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "swapper".to_string(), + amount: vec![], + }.into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Transfers No Coin Balance")] +fn test_execute_swap(params: Params) -> ContractResult<()> { + // Convert params contract balance to a slice + let contract_balance: &[Coin] = ¶ms.contract_balance; + + // Create mock dependencies + let mut deps = mock_dependencies_with_balances(&[("swap_contract_address", contract_balance)]); + + // Create mock env + let mut env = mock_env(); + env.contract.address = Addr::unchecked("swap_contract_address"); + + // Create mock info + let info = mock_info("swap_contract_address", &[]); + + // Call execute_swap with the given test parameters + let res = skip_swap_neutron_astroport_swap::contract::execute( + deps.as_mut(), + env, + info, + ExecuteMsg::TransferFundsBack { + caller: Addr::unchecked("swapper"), + }, + ); + + // Assert the behavior is correct + match res { + Ok(res) => { + // Assert the test did not expect an error + assert!( + params.expected_error.is_none(), + "expected test to error with {:?}, but it succeeded", + params.expected_error + ); + + // Assert the messages are correct + assert_eq!(res.messages, params.expected_messages); + } + Err(err) => { + // Assert the test expected an error + assert!( + params.expected_error.is_some(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err, params.expected_error.unwrap()); + } + } + + Ok(()) +} diff --git a/contracts/networks/osmosis/ibc-transfer/Cargo.toml b/contracts/networks/osmosis/ibc-transfer/Cargo.toml new file mode 100644 index 00000000..ce13ecd7 --- /dev/null +++ b/contracts/networks/osmosis/ibc-transfer/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "skip-swap-osmosis-ibc-transfer" +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-std = { workspace = true } +cw-storage-plus = { workspace = true } +ibc-proto = { workspace = true } +prost = { workspace = true } +serde-json-wasm = { workspace = true } +serde-cw-value = { workspace = true } +skip = { path = "../../../../packages/skip" } +thiserror = { workspace = true } + +[dev-dependencies] +test-case = { workspace = true } \ No newline at end of file diff --git a/contracts/networks/osmosis/ibc-transfer/src/contract.rs b/contracts/networks/osmosis/ibc-transfer/src/contract.rs new file mode 100644 index 00000000..830e3d4f --- /dev/null +++ b/contracts/networks/osmosis/ibc-transfer/src/contract.rs @@ -0,0 +1,259 @@ +use crate::{ + error::{ContractError, ContractResult}, + state::{ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER, IN_PROGRESS_IBC_TRANSFER}, +}; +use cosmwasm_std::{ + entry_point, to_binary, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, + Reply, Response, SubMsg, SubMsgResult, +}; +use ibc_proto::ibc::applications::transfer::v1::{MsgTransfer, MsgTransferResponse}; +use prost::Message; +use serde_cw_value::Value; +use skip::{ + ibc::{ + AckID, ExecuteMsg, IbcInfo, IbcLifecycleComplete, InstantiateMsg, + OsmosisInProgressIbcTransfer as InProgressIbcTransfer, OsmosisQueryMsg as QueryMsg, + }, + proto_coin::ProtoCoin, + sudo::{OsmosisSudoMsg as SudoMsg, SudoType}, +}; + +const IBC_MSG_TRANSFER_TYPE_URL: &str = "/ibc.applications.transfer.v1.MsgTransfer"; +const REPLY_ID: u64 = 1; + +/////////////////// +/// INSTANTIATE /// +/////////////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: InstantiateMsg, +) -> ContractResult { + Ok(Response::new().add_attribute("action", "instantiate")) +} + +/////////////// +/// EXECUTE /// +/////////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + _info: MessageInfo, + msg: ExecuteMsg, +) -> ContractResult { + match msg { + ExecuteMsg::IbcTransfer { + info, + coin, + timeout_timestamp, + } => execute_ibc_transfer(deps, env, info, coin, timeout_timestamp), + } +} + +// Converts the given info and coin into an ibc transfer message, +// saves necessary info in case the ibc transfer fails to send funds back to +// a recovery address, and then emits the ibc transfer message as a sub message +fn execute_ibc_transfer( + deps: DepsMut, + env: Env, + info: IbcInfo, + coin: Coin, + timeout_timestamp: u64, +) -> ContractResult { + // Save in progress ibc transfer data (recover address and coin) to storage, to be used in sudo handler + IN_PROGRESS_IBC_TRANSFER.save( + deps.storage, + &InProgressIbcTransfer { + recover_address: info.recover_address, // This address is verified in entry point + coin: coin.clone(), + channel_id: info.source_channel.clone(), + }, + )?; + + // Verify memo is valid json and add the necessary key/value pair to trigger the ibc hooks callback logic. + let memo = verify_and_create_memo(info.memo, env.contract.address.to_string())?; + + // Create osmosis ibc transfer message + let msg = MsgTransfer { + source_port: "transfer".to_string(), + source_channel: info.source_channel, + token: Some(ProtoCoin(coin).into()), + sender: env.contract.address.to_string(), + receiver: info.receiver, + timeout_height: None, + timeout_timestamp, + memo, + }; + + // Create stargate message from osmosis ibc transfer message + let msg = CosmosMsg::Stargate { + type_url: IBC_MSG_TRANSFER_TYPE_URL.to_string(), + value: msg.encode_to_vec().into(), + }; + + // Create sub message from osmosis ibc transfer message to receive a reply + let sub_msg = SubMsg::reply_on_success(msg, REPLY_ID); + + Ok(Response::new() + .add_submessage(sub_msg) + .add_attribute("action", "execute_ibc_transfer")) +} + +///////////// +/// REPLY /// +///////////// + +// Handles the reply from the ibc transfer sub message +// Upon success, maps the sub msg AckID (channel_id, sequence_id) +// to the in progress ibc transfer struct, and saves it to storage. +// Now that the map entry is stored, it also removes the in progress +// ibc transfer from storage. +#[entry_point] +pub fn reply(deps: DepsMut, _env: Env, reply: Reply) -> ContractResult { + // Error if the reply id is not the same as the one used in the sub message dispatched + // This should never happen since we are using a constant reply id, but added in case + // the wasm module doesn't behave as expected. + if reply.id != REPLY_ID { + unreachable!() + } + + // Get the sub message response from the reply and error if it does not exist + // This should never happen since sub msg was set to reply on success only, + // but added in case the wasm module doesn't behave as expected. + let SubMsgResult::Ok(sub_msg_response) = reply.result else { + unreachable!() + }; + + // Parse the response from the sub message + let resp: MsgTransferResponse = MsgTransferResponse::decode( + sub_msg_response + .data + .ok_or(ContractError::MissingResponseData)? + .as_slice(), + )?; + + // Get and delete the in progress ibc transfer from storage + let in_progress_ibc_transfer = IN_PROGRESS_IBC_TRANSFER.load(deps.storage)?; + IN_PROGRESS_IBC_TRANSFER.remove(deps.storage); + + // Set ack_id to be the channel id and sequence id from the response as a tuple + let ack_id: AckID = (&in_progress_ibc_transfer.channel_id, resp.sequence); + + // Error if unique ack_id (channel id, sequence id) already exists in storage + if ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER.has(deps.storage, ack_id) { + return Err(ContractError::AckIDAlreadyExists { + channel_id: ack_id.0.into(), + sequence_id: ack_id.1, + }); + } + + // Set the in progress ibc transfer to storage, keyed by channel id and sequence id + ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER.save(deps.storage, ack_id, &in_progress_ibc_transfer)?; + + Ok(Response::new().add_attribute("action", "sub_msg_reply_success")) +} + +//////////// +/// SUDO /// +//////////// + +// Handles the ibc callback from the ibc hooks module +// Upon success, removes the in progress ibc transfer from storage and returns immediately. +// Upon error or timeout, sends the attempted ibc transferred funds back to the user's recover address. +#[entry_point] +pub fn sudo(deps: DepsMut, _env: Env, msg: SudoMsg) -> ContractResult { + // Get the channel id, sequence id, and sudo type from the sudo message + let (channel, sequence, sudo_type) = match msg { + SudoMsg::IbcLifecycleComplete(IbcLifecycleComplete::IbcAck { + channel, + sequence, + ack: _, + success, + }) => { + // Remove the AckID <> in progress ibc transfer from storage + // and return immediately if the ibc transfer was successful + // since no further action is needed. + if success { + let ack_id: AckID = (&channel, sequence); + ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER.remove(deps.storage, ack_id); + + return Ok(Response::new().add_attribute("action", SudoType::Response)); + } + + (channel, sequence, SudoType::Error) + } + SudoMsg::IbcLifecycleComplete(IbcLifecycleComplete::IbcTimeout { channel, sequence }) => { + (channel, sequence, SudoType::Timeout) + } + }; + + // Get and remove the AckID <> in progress ibc transfer from storage + let ack_id: AckID = (&channel, sequence); + let in_progress_ibc_transfer = ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER.load(deps.storage, ack_id)?; + ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER.remove(deps.storage, ack_id); + + // Create bank send message to send funds back to user's recover address + let bank_send_msg = BankMsg::Send { + to_address: in_progress_ibc_transfer.recover_address, + amount: vec![in_progress_ibc_transfer.coin], + }; + + Ok(Response::new() + .add_message(bank_send_msg) + .add_attribute("action", sudo_type)) +} + +//////////////////////// +/// HELPER FUNCTIONS /// +//////////////////////// + +// Verifies the given memo is empty or valid json, and then adds the necessary +// key/value pair to trigger the ibc hooks callback logic. +fn verify_and_create_memo(memo: String, contract_address: String) -> ContractResult { + // If the memo given is empty, then set it to "{}" to avoid json parsing errors. Then, + // get Value object from json string, erroring if the memo was not null while not being valid json + let mut memo: Value = serde_json_wasm::from_str(if memo.is_empty() { "{}" } else { &memo })?; + + // Transform the Value object into a Value map representation of the json string + // and insert the necessary key value pair into the memo map to trigger + // the ibc hooks callback logic. That key value pair is: + // { "ibc_callback": } + // + // If the "ibc_callback" key was already set, this will override + // the value with the current contract address. + if let Value::Map(ref mut memo) = memo { + memo.insert( + Value::String("ibc_callback".to_string()), + Value::String(contract_address), + ) + } else { + unreachable!() + }; + + // Transform the memo Value map back into a json string + let memo = serde_json_wasm::to_string(&memo)?; + + Ok(memo) +} + +///////////// +/// QUERY /// +///////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> ContractResult { + match msg { + QueryMsg::InProgressIbcTransfer { + channel_id, + sequence_id, + } => to_binary( + &ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER.load(deps.storage, (&channel_id, sequence_id))?, + ), + } + .map_err(From::from) +} diff --git a/contracts/networks/osmosis/ibc-transfer/src/error.rs b/contracts/networks/osmosis/ibc-transfer/src/error.rs new file mode 100644 index 00000000..3c8ed3e4 --- /dev/null +++ b/contracts/networks/osmosis/ibc-transfer/src/error.rs @@ -0,0 +1,28 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +pub type ContractResult = core::result::Result; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error(transparent)] + Decode(#[from] prost::DecodeError), + + #[error(transparent)] + JsonDecode(#[from] serde_json_wasm::de::Error), + + #[error(transparent)] + JsonEncode(#[from] serde_json_wasm::ser::Error), + + #[error("SubMsgResponse does not contain data")] + MissingResponseData, + + #[error("ACK ID already exists for channel ID {channel_id} and sequence ID {sequence_id}")] + AckIDAlreadyExists { + channel_id: String, + sequence_id: u64, + }, +} diff --git a/contracts/networks/osmosis/ibc-transfer/src/lib.rs b/contracts/networks/osmosis/ibc-transfer/src/lib.rs new file mode 100644 index 00000000..3d3e89c8 --- /dev/null +++ b/contracts/networks/osmosis/ibc-transfer/src/lib.rs @@ -0,0 +1,3 @@ +pub mod contract; +pub mod error; +pub mod state; diff --git a/contracts/networks/osmosis/ibc-transfer/src/state.rs b/contracts/networks/osmosis/ibc-transfer/src/state.rs new file mode 100644 index 00000000..345f33ff --- /dev/null +++ b/contracts/networks/osmosis/ibc-transfer/src/state.rs @@ -0,0 +1,7 @@ +use cw_storage_plus::{Item, Map}; +use skip::ibc::{AckID, OsmosisInProgressIbcTransfer as InProgressIbcTransfer}; + +pub const IN_PROGRESS_IBC_TRANSFER: Item = + Item::new("in_progress_ibc_transfer"); +pub const ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER: Map = + Map::new("ack_id_to_transferer"); diff --git a/contracts/networks/osmosis/ibc-transfer/tests/test_execute_ibc_transfer.rs b/contracts/networks/osmosis/ibc-transfer/tests/test_execute_ibc_transfer.rs new file mode 100644 index 00000000..df179b6c --- /dev/null +++ b/contracts/networks/osmosis/ibc-transfer/tests/test_execute_ibc_transfer.rs @@ -0,0 +1,266 @@ +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env, mock_info}, + Addr, Coin, + ReplyOn::Success, + SubMsg, +}; +use ibc_proto::cosmos::base::v1beta1::Coin as IbcCoin; +use ibc_proto::ibc::applications::transfer::v1::MsgTransfer; +use prost::Message; +use skip::ibc::{ + ExecuteMsg, IbcFee, IbcInfo, OsmosisInProgressIbcTransfer as InProgressIBCTransfer, +}; +use skip_swap_osmosis_ibc_transfer::{error::ContractResult, state::IN_PROGRESS_IBC_TRANSFER}; +use test_case::test_case; + +/* +Test Cases: + +Expect Response (Output Message Is Correct, In Progress Ibc Transfer Is Saved, No Error) + - Empty String Memo + - Override Already Set Ibc Callback Memo + - Add Ibc Callback Key/Value Pair To Other Key/Value In Memo + +Expect Error + - Non Empty String, Invalid Json Memo + + */ + +// Define test parameters +struct Params { + ibc_adapter_contract_address: Addr, + coin: Coin, + ibc_info: IbcInfo, + timeout_timestamp: u64, + expected_messages: Vec, + expected_in_progress_ibc_transfer: InProgressIBCTransfer, + expected_error_string: String, +} + +// Test execute_ibc_transfer +#[test_case( + Params { + ibc_adapter_contract_address: Addr::unchecked("ibc_transfer".to_string()), + coin: Coin::new(100, "osmo"), + ibc_info: IbcInfo { + source_channel: "source_channel".to_string(), + receiver: "receiver".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![], + timeout_fee: vec![], + }, + memo: "".to_string(), + recover_address: "recover_address".to_string(), + }, + timeout_timestamp: 100, + expected_messages: vec![SubMsg { + id: 1, + msg: cosmwasm_std::CosmosMsg::Stargate { + type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), + value: MsgTransfer { + source_port: "transfer".to_string(), + source_channel: "source_channel".to_string(), + token: Some(IbcCoin { + denom: "osmo".to_string(), + amount: "100".to_string(), + }), + sender: "ibc_transfer".to_string(), + receiver: "receiver".to_string(), + timeout_height: None, + timeout_timestamp: 100, + memo: r#"{"ibc_callback":"ibc_transfer"}"#.to_string(), + } + .encode_to_vec().into(), + }, + gas_limit: None, + reply_on: Success, + } + ], + expected_in_progress_ibc_transfer: InProgressIBCTransfer { + recover_address: "recover_address".to_string(), + coin: Coin::new(100, "osmo"), + channel_id: "source_channel".to_string(), + }, + expected_error_string: "".to_string(), + }; + "Empty String Memo")] +#[test_case( + Params { + ibc_adapter_contract_address: Addr::unchecked("ibc_transfer".to_string()), + coin: Coin::new(100, "osmo"), + ibc_info: IbcInfo { + source_channel: "source_channel".to_string(), + receiver: "receiver".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![], + timeout_fee: vec![], + }, + memo: r#"{"ibc_callback":"random_address"}"#.to_string(), + recover_address: "recover_address".to_string(), + }, + timeout_timestamp: 100, + expected_messages: vec![SubMsg { + id: 1, + msg: cosmwasm_std::CosmosMsg::Stargate { + type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), + value: MsgTransfer { + source_port: "transfer".to_string(), + source_channel: "source_channel".to_string(), + token: Some(IbcCoin { + denom: "osmo".to_string(), + amount: "100".to_string(), + }), + sender: "ibc_transfer".to_string(), + receiver: "receiver".to_string(), + timeout_height: None, + timeout_timestamp: 100, + memo: r#"{"ibc_callback":"ibc_transfer"}"#.to_string(), + } + .encode_to_vec().into(), + }, + gas_limit: None, + reply_on: Success, + } + ], + expected_in_progress_ibc_transfer: InProgressIBCTransfer { + recover_address: "recover_address".to_string(), + coin: Coin::new(100, "osmo"), + channel_id: "source_channel".to_string(), + }, + expected_error_string: "".to_string(), + }; + "Override Already Set Ibc Callback Memo")] +#[test_case( + Params { + ibc_adapter_contract_address: Addr::unchecked("ibc_transfer".to_string()), + coin: Coin::new(100, "osmo"), + ibc_info: IbcInfo { + source_channel: "source_channel".to_string(), + receiver: "receiver".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![], + timeout_fee: vec![], + }, + memo: r#"{"pfm":"example_value","wasm":"example_contract"}"#.to_string(), + recover_address: "recover_address".to_string(), + }, + timeout_timestamp: 100, + expected_messages: vec![SubMsg { + id: 1, + msg: cosmwasm_std::CosmosMsg::Stargate { + type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), + value: MsgTransfer { + source_port: "transfer".to_string(), + source_channel: "source_channel".to_string(), + token: Some(IbcCoin { + denom: "osmo".to_string(), + amount: "100".to_string(), + }), + sender: "ibc_transfer".to_string(), + receiver: "receiver".to_string(), + timeout_height: None, + timeout_timestamp: 100, + memo: r#"{"ibc_callback":"ibc_transfer","pfm":"example_value","wasm":"example_contract"}"#.to_string(), + } + .encode_to_vec().into(), + }, + gas_limit: None, + reply_on: Success, + } + ], + expected_in_progress_ibc_transfer: InProgressIBCTransfer { + recover_address: "recover_address".to_string(), + coin: Coin::new(100, "osmo"), + channel_id: "source_channel".to_string(), + }, + expected_error_string: "".to_string(), + }; + "Add Ibc Callback Key/Value Pair To Other Key/Value In Memo")] +#[test_case( + Params { + ibc_adapter_contract_address: Addr::unchecked("ibc_transfer".to_string()), + coin: Coin::new(100, "osmo"), + ibc_info: IbcInfo { + source_channel: "source_channel".to_string(), + receiver: "receiver".to_string(), + fee: IbcFee { + recv_fee: vec![], + ack_fee: vec![], + timeout_fee: vec![], + }, + memo: "{invalid}".to_string(), + recover_address: "recover_address".to_string(), + }, + timeout_timestamp: 100, + expected_messages: vec![], + expected_in_progress_ibc_transfer: InProgressIBCTransfer { + recover_address: "recover_address".to_string(), + coin: Coin::new(100, "osmo"), + channel_id: "source_channel".to_string(), + }, + expected_error_string: "Object key is not a string.".to_string(), + }; + "Non Empty String, Invalid Json Memo - Expect Error")] +fn test_execute_ibc_transfer(params: Params) -> ContractResult<()> { + // Create mock dependencies + let mut deps = mock_dependencies(); + + // Create mock env + let mut env = mock_env(); + env.contract.address = params.ibc_adapter_contract_address.clone(); + + // Create mock info + let info = mock_info("caller", &[]); + + // Call execute_ibc_transfer with the given test parameters + let res = skip_swap_osmosis_ibc_transfer::contract::execute( + deps.as_mut(), + env, + info, + ExecuteMsg::IbcTransfer { + info: params.ibc_info.clone(), + coin: params.coin.clone(), + timeout_timestamp: params.timeout_timestamp, + }, + ); + + // Assert the behavior is correct + match res { + Ok(res) => { + // Assert the test did not expect an error + assert!( + params.expected_error_string.is_empty(), + "expected test to error with {:?}, but it succeeded", + params.expected_error_string + ); + + // Assert the messages in the response are correct + assert_eq!(res.messages, params.expected_messages); + + // Load the in progress ibc transfer from state and verify it is correct + let stored_in_progress_ibc_transfer = IN_PROGRESS_IBC_TRANSFER.load(&deps.storage)?; + + // Assert the in progress ibc transfer is correct + assert_eq!( + stored_in_progress_ibc_transfer, + params.expected_in_progress_ibc_transfer + ); + } + Err(err) => { + // Assert the test expected an error + assert!( + !params.expected_error_string.is_empty(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err.to_string(), params.expected_error_string); + } + } + + Ok(()) +} diff --git a/contracts/networks/osmosis/ibc-transfer/tests/test_reply.rs b/contracts/networks/osmosis/ibc-transfer/tests/test_reply.rs new file mode 100644 index 00000000..dfba44bc --- /dev/null +++ b/contracts/networks/osmosis/ibc-transfer/tests/test_reply.rs @@ -0,0 +1,241 @@ +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env}, + Coin, Reply, StdError, SubMsgResponse, SubMsgResult, +}; +use ibc_proto::ibc::applications::transfer::v1::MsgTransferResponse; +use prost::Message; +use skip::ibc::OsmosisInProgressIbcTransfer as InProgressIBCTransfer; +use skip_swap_osmosis_ibc_transfer::{ + error::ContractResult, + state::{ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER, IN_PROGRESS_IBC_TRANSFER}, +}; +use test_case::test_case; + +/* +Test Cases: + +Expect Success + - Happy Path (tests the in progress ibc transfer is removed from storage and the ack id to in progress ibc transfer map entry is correct) + +Expect Error + - Missing Sub Msg Response Data + - Invalid Sub Msg Response Data To Convert To MsgTransferResponse + - No In Progress Ibc Transfer To Load + - Ack ID Already Exists + - SubMsg Incorrect Reply ID + +Expect Panic + - SubMsgResult Error + - Should panic because the sub msg is set to reply only on success, so should never happen + unless the wasm module worked unexpectedly + - SubMsg Incorrect Reply ID + - Should panic because the reply id is set to a constant, so should never happen unless + the wasm module worked unexpectedly + */ + +// Define test parameters +struct Params { + channel_id: String, + sequence_id: u64, + reply: Reply, + pre_reply_in_progress_ibc_transfer: Option, + store_ack_id_to_in_progress_ibc_transfer: bool, + expected_error_string: String, +} + +// Test reply +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 5, + reply: Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(MsgTransferResponse {sequence: 5}.encode_to_vec().as_slice().into()), + }), + }, + pre_reply_in_progress_ibc_transfer: Some(InProgressIBCTransfer { + recover_address: "recover_address".to_string(), + coin: Coin::new(100, "osmo"), + channel_id: "channel_id".to_string(), + }), + store_ack_id_to_in_progress_ibc_transfer: false, + expected_error_string: "".to_string(), + }; + "Happy Path")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + reply: Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: None, + }), + }, + pre_reply_in_progress_ibc_transfer: None, + store_ack_id_to_in_progress_ibc_transfer: false, + expected_error_string: "SubMsgResponse does not contain data".to_string(), + }; + "Missing Sub Msg Response Data - Expect Error")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + reply: Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(b"invalid".into()), + }), + }, + pre_reply_in_progress_ibc_transfer: None, + store_ack_id_to_in_progress_ibc_transfer: false, + expected_error_string: "failed to decode Protobuf message: buffer underflow".to_string(), + }; + "Invalid Sub Msg Response Data To Convert To MsgTransferResponse - Expect Error")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + reply: Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(MsgTransferResponse {sequence: 5}.encode_to_vec().as_slice().into()), + }), + }, + pre_reply_in_progress_ibc_transfer: None, + store_ack_id_to_in_progress_ibc_transfer: false, + expected_error_string: "skip::ibc::OsmosisInProgressIbcTransfer not found".to_string(), + }; + "No In Progress Ibc Transfer To Load - Expect Error")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 5, + reply: Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(MsgTransferResponse {sequence: 5}.encode_to_vec().as_slice().into()), + }), + }, + pre_reply_in_progress_ibc_transfer: Some(InProgressIBCTransfer { + recover_address: "recover_address".to_string(), + coin: Coin::new(100, "osmo"), + channel_id: "channel_id".to_string(), + }), + store_ack_id_to_in_progress_ibc_transfer: true, + expected_error_string: "ACK ID already exists for channel ID channel_id and sequence ID 5".to_string(), + }; + "Ack ID Already Exists - Expect Error")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + reply: Reply { + id: 2, + result: SubMsgResult::Err("".to_string()), + }, + pre_reply_in_progress_ibc_transfer: Some(InProgressIBCTransfer { + recover_address: "recover_address".to_string(), + coin: Coin::new(100, "osmo"), + channel_id: "channel_id".to_string(), + }), + store_ack_id_to_in_progress_ibc_transfer: false, + expected_error_string: "".to_string(), + } => panics "internal error: entered unreachable code"; + "SubMsg Incorrect Reply ID - Expect Panic")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + reply: Reply { + id: 1, + result: SubMsgResult::Err("".to_string()), + }, + pre_reply_in_progress_ibc_transfer: Some(InProgressIBCTransfer { + recover_address: "recover_address".to_string(), + coin: Coin::new(100, "osmo"), + channel_id: "channel_id".to_string(), + }), + expected_error_string: "".to_string(), + store_ack_id_to_in_progress_ibc_transfer: false, + } => panics "internal error: entered unreachable code"; + "SubMsgResult Error - Expect Panic")] +fn test_reply(params: Params) -> ContractResult<()> { + // Create mock dependencies + let mut deps = mock_dependencies(); + + // Create mock env + let env = mock_env(); + + // Store the in progress ibc transfer to state if it exists + if let Some(in_progress_ibc_transfer) = params.pre_reply_in_progress_ibc_transfer.clone() { + IN_PROGRESS_IBC_TRANSFER.save(deps.as_mut().storage, &in_progress_ibc_transfer)?; + } + + // If the test expects the ack id to in progress ibc transfer map entry to be stored, + // store it to state + if params.store_ack_id_to_in_progress_ibc_transfer { + ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER.save( + deps.as_mut().storage, + (¶ms.channel_id, params.sequence_id), + ¶ms.pre_reply_in_progress_ibc_transfer.clone().unwrap(), + )?; + } + + // Call reply with the given test parameters + let res = skip_swap_osmosis_ibc_transfer::contract::reply(deps.as_mut(), env, params.reply); + + // Assert the behavior is correct + match res { + Ok(_) => { + // Assert the test did not expect an error + assert!( + params.expected_error_string.is_empty(), + "expected test to error with {:?}, but it succeeded", + params.expected_error_string + ); + + // Verify the in progress ibc transfer was removed from storage + match IN_PROGRESS_IBC_TRANSFER.load(&deps.storage) { + Ok(in_progress_ibc_transfer) => { + panic!( + "expected in progress ibc transfer to be removed: {:?}", + in_progress_ibc_transfer + ) + } + Err(err) => assert_eq!( + err, + StdError::NotFound { + kind: "skip::ibc::OsmosisInProgressIbcTransfer".to_string() + } + ), + }; + + // Verify the stored ack id to in progress ibc transfer map entry is correct + assert_eq!( + ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER + .load(&deps.storage, (¶ms.channel_id, params.sequence_id))?, + params.pre_reply_in_progress_ibc_transfer.unwrap() + ); + } + Err(err) => { + // Assert the test expected an error + assert!( + !params.expected_error_string.is_empty(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err.to_string(), params.expected_error_string); + } + } + + Ok(()) +} diff --git a/contracts/networks/osmosis/ibc-transfer/tests/test_sudo.rs b/contracts/networks/osmosis/ibc-transfer/tests/test_sudo.rs new file mode 100644 index 00000000..36df049c --- /dev/null +++ b/contracts/networks/osmosis/ibc-transfer/tests/test_sudo.rs @@ -0,0 +1,204 @@ +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env}, + BankMsg, Coin, + ReplyOn::Never, + StdError, SubMsg, +}; +use skip::{ + ibc::{IbcLifecycleComplete, OsmosisInProgressIbcTransfer as InProgressIBCTransfer}, + sudo::OsmosisSudoMsg as SudoMsg, +}; +use skip_swap_osmosis_ibc_transfer::{ + error::ContractResult, state::ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER, +}; +use test_case::test_case; + +/* +Test Cases: + +Expect Success + - Sudo Response - Happy Path Response + - Sudo Timeout - Send Failed Ibc Coin To Recover Address + - Sudo Error - Send Failed Ibc Coin To Recover Address + +Expect Error + - No In Progress Ibc Transfer Mapped To Sudo Ack ID - Expect Error + + */ + +// Define test parameters +struct Params { + channel_id: String, + sequence_id: u64, + sudo_msg: SudoMsg, + stored_in_progress_ibc_transfer: Option, + expected_messages: Vec, + expected_error_string: String, +} + +// Test sudo +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + sudo_msg: SudoMsg::IbcLifecycleComplete(IbcLifecycleComplete::IbcAck{ + channel: "channel_id".to_string(), + sequence: 1, + ack: "".to_string(), + success: true, + }), + stored_in_progress_ibc_transfer: Some(InProgressIBCTransfer { + recover_address: "recover_address".to_string(), + coin: Coin::new(100, "osmo"), + channel_id: "channel_id".to_string(), + }), + expected_messages: vec![], + expected_error_string: "".to_string(), + }; + "Sudo Response - Happy Path - Send Timeout Fee")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + sudo_msg: SudoMsg::IbcLifecycleComplete(IbcLifecycleComplete::IbcTimeout{ + channel: "channel_id".to_string(), + sequence: 1, + }), + stored_in_progress_ibc_transfer: Some(InProgressIBCTransfer { + recover_address: "recover_address".to_string(), + coin: Coin::new(100, "osmo"), + channel_id: "channel_id".to_string(), + }), + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "recover_address".to_string(), + amount: vec![Coin::new(100, "osmo")], + }.into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error_string: "".to_string(), + }; + "Sudo Timeout - Send Failed Ibc Coin")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + sudo_msg: SudoMsg::IbcLifecycleComplete(IbcLifecycleComplete::IbcAck{ + channel: "channel_id".to_string(), + sequence: 1, + ack: "".to_string(), + success: false, + }), + stored_in_progress_ibc_transfer: Some(InProgressIBCTransfer { + recover_address: "recover_address".to_string(), + coin: Coin::new(100, "osmo"), + channel_id: "channel_id".to_string(), + }), + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "recover_address".to_string(), + amount: vec![Coin::new(100, "osmo")], + }.into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error_string: "".to_string(), + }; + "Sudo Error - Send Failed Ibc Coin To Recover Address")] +#[test_case( + Params { + channel_id: "channel_id".to_string(), + sequence_id: 1, + sudo_msg: SudoMsg::IbcLifecycleComplete(IbcLifecycleComplete::IbcAck{ + channel: "channel_id".to_string(), + sequence: 1, + ack: "".to_string(), + success: false, + }), + stored_in_progress_ibc_transfer: None, + expected_messages: vec![], + expected_error_string: "skip::ibc::OsmosisInProgressIbcTransfer not found".to_string(), + }; + "No In Progress Ibc Transfer Mapped To Sudo Ack ID - Expect Error")] +fn test_sudo(params: Params) -> ContractResult<()> { + // Create mock dependencies + let mut deps = mock_dependencies(); + + // Create mock env + let env = mock_env(); + + // Store the in progress ibc transfer to state if it exists + if let Some(in_progress_ibc_transfer) = params.stored_in_progress_ibc_transfer.clone() { + ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER.save( + deps.as_mut().storage, + (¶ms.channel_id, params.sequence_id), + &in_progress_ibc_transfer, + )?; + } + + // Call sudo with the given test parameters + let res = skip_swap_osmosis_ibc_transfer::contract::sudo(deps.as_mut(), env, params.sudo_msg); + + // Assert the behavior is correct + match res { + Ok(res) => { + // Assert the test did not expect an error + assert!( + params.expected_error_string.is_empty(), + "expected test to error with {:?}, but it succeeded", + params.expected_error_string + ); + + // Verify the in progress ibc transfer was removed from storage + match ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER + .load(&deps.storage, (¶ms.channel_id, params.sequence_id)) + { + Ok(in_progress_ibc_transfer) => { + panic!( + "expected in progress ibc transfer to be removed: {:?}", + in_progress_ibc_transfer + ) + } + Err(err) => assert_eq!( + err, + StdError::NotFound { + kind: "skip::ibc::OsmosisInProgressIbcTransfer".to_string() + } + ), + }; + + // Verify the messages in the response are correct + assert_eq!(res.messages, params.expected_messages); + } + Err(err) => { + println!("Here"); + // Assert the test expected an error + assert!( + !params.expected_error_string.is_empty(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err.to_string(), params.expected_error_string); + + if params.stored_in_progress_ibc_transfer.is_some() { + // Verify the ack id to in progress ibc transfer map entry is still stored + assert_eq!( + ACK_ID_TO_IN_PROGRESS_IBC_TRANSFER + .load(&deps.storage, (¶ms.channel_id, params.sequence_id))?, + params.stored_in_progress_ibc_transfer.unwrap() + ); + } + } + } + + Ok(()) +} diff --git a/contracts/networks/osmosis/swap/Cargo.toml b/contracts/networks/osmosis/swap/Cargo.toml new file mode 100644 index 00000000..275ffc99 --- /dev/null +++ b/contracts/networks/osmosis/swap/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "skip-swap-osmosis-poolmanager-swap" +version = { workspace = true } +rust-version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +documentation = { workspace = true } +keywords = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-utils = { workspace = true } +osmosis-std = { workspace = true } +skip = { path = "../../../../packages/skip" } +thiserror = { workspace = true } + +[dev-dependencies] +test-case = { workspace = true } \ No newline at end of file diff --git a/contracts/networks/osmosis/swap/src/contract.rs b/contracts/networks/osmosis/swap/src/contract.rs new file mode 100644 index 00000000..0fde3b66 --- /dev/null +++ b/contracts/networks/osmosis/swap/src/contract.rs @@ -0,0 +1,230 @@ +use crate::error::{ContractError, ContractResult}; +use cosmwasm_std::{ + entry_point, to_binary, Addr, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, + MessageInfo, Response, Uint128, WasmMsg, +}; +use cw_utils::one_coin; +use osmosis_std::types::osmosis::poolmanager::v1beta1::{ + EstimateSwapExactAmountInResponse, EstimateSwapExactAmountOutResponse, MsgSwapExactAmountIn, + PoolmanagerQuerier, SwapAmountInRoute, SwapAmountOutRoute, +}; +use skip::{ + proto_coin::ProtoCoin, + swap::{ + convert_swap_operations, ExecuteMsg, OsmosisInstantiateMsg as InstantiateMsg, QueryMsg, + SwapOperation, + }, +}; +use std::str::FromStr; + +/////////////////// +/// INSTANTIATE /// +/////////////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: InstantiateMsg, +) -> ContractResult { + Ok(Response::new().add_attribute("action", "instantiate")) +} + +/////////////// +/// EXECUTE /// +/////////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> ContractResult { + match msg { + ExecuteMsg::Swap { operations } => execute_swap(env, info, operations), + ExecuteMsg::TransferFundsBack { caller } => execute_transfer_funds_back(deps, env, caller), + } +} + +// Executes a swap with the given swap operations and then transfers the funds back to the caller +fn execute_swap( + env: Env, + info: MessageInfo, + operations: Vec, +) -> ContractResult { + // Get coin in from the message info, error if there is not exactly one coin sent + let coin_in = one_coin(&info)?; + + // Create the osmosis poolmanager swap exact amount in message + let swap_msg = create_osmosis_swap_msg(&env, coin_in, operations)?; + + // Create the transfer funds back message + let transfer_funds_back_msg = WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_binary(&ExecuteMsg::TransferFundsBack { + caller: info.sender, + })?, + funds: vec![], + }; + + Ok(Response::new() + .add_message(swap_msg) + .add_message(transfer_funds_back_msg) + .add_attribute("action", "dispatch_swap_and_transfer_back")) +} + +// Query the contract's balance and transfer the funds back to the caller +fn execute_transfer_funds_back(deps: DepsMut, env: Env, caller: Addr) -> ContractResult { + // Create the bank message send to transfer the contract funds back to the caller + let transfer_funds_back_msg = BankMsg::Send { + to_address: caller.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")) +} + +//////////////////////// +/// HELPER FUNCTIONS /// +//////////////////////// + +// Creates the osmosis poolmanager swap exact amount in message +fn create_osmosis_swap_msg( + env: &Env, + coin_in: Coin, + swap_operations: Vec, +) -> ContractResult { + // Convert the swap operations to osmosis swap amount in routes + // Return an error if there was an error converting the swap + // operations to osmosis swap amount in routes. + let osmosis_swap_amount_in_routes: Vec = + convert_swap_operations(swap_operations).map_err(ContractError::ParseIntPoolID)?; + + // Create the osmosis poolmanager swap exact amount in message + // The token out min amount is set to 1 because we are not concerned + // with the minimum amount in this contract, that gets verified in the + // entry point contract. + let swap_msg: CosmosMsg = MsgSwapExactAmountIn { + sender: env.contract.address.to_string(), + routes: osmosis_swap_amount_in_routes, + token_in: Some(ProtoCoin(coin_in).into()), + token_out_min_amount: "1".to_string(), + } + .into(); + + Ok(swap_msg) +} + +///////////// +/// QUERY /// +///////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> ContractResult { + match msg { + QueryMsg::SimulateSwapExactCoinIn { + coin_in, + swap_operations, + } => to_binary(&query_simulate_swap_exact_coin_in( + deps, + coin_in, + swap_operations, + )?), + QueryMsg::SimulateSwapExactCoinOut { + coin_out, + swap_operations, + } => to_binary(&query_simulate_swap_exact_coin_out( + deps, + coin_out, + swap_operations, + )?), + _ => { + unimplemented!() + } + } + .map_err(From::from) +} + +// Queries the osmosis poolmanager module to simulate a swap exact amount in +fn query_simulate_swap_exact_coin_in( + deps: Deps, + coin_in: Coin, + swap_operations: Vec, +) -> ContractResult { + // Error if swap operations is empty + let (Some(first_op), Some(last_op)) = (swap_operations.first(), swap_operations.last()) else { + return Err(ContractError::SwapOperationsEmpty); + }; + + // Ensure coin_in's denom is the same as the first swap operation's denom in + if coin_in.denom != first_op.denom_in { + return Err(ContractError::CoinInDenomMismatch); + } + + // Get denom out from last swap operation to be used as the return coin's denom + let denom_out = last_op.denom_out.clone(); + + // Convert the swap operations to osmosis swap amount in routes + // Return an error if there was an error converting the swap + // operations to osmosis swap amount in routes. + let osmosis_swap_amount_in_routes: Vec = + convert_swap_operations(swap_operations).map_err(ContractError::ParseIntPoolID)?; + + // Query the osmosis poolmanager module to simulate the swap exact amount in + let res: EstimateSwapExactAmountInResponse = PoolmanagerQuerier::new(&deps.querier) + .estimate_swap_exact_amount_in( + osmosis_swap_amount_in_routes.first().unwrap().pool_id, + coin_in.to_string(), + osmosis_swap_amount_in_routes, + )?; + + // Return the coin out + Ok(Coin { + denom: denom_out, + amount: Uint128::from_str(&res.token_out_amount)?, + }) +} + +// Queries the osmosis poolmanager module to simulate a swap exact amount out +fn query_simulate_swap_exact_coin_out( + deps: Deps, + coin_out: Coin, + swap_operations: Vec, +) -> ContractResult { + // Error if swap operations is empty + let (Some(first_op), Some(last_op)) = (swap_operations.first(), swap_operations.last()) else { + return Err(ContractError::SwapOperationsEmpty); + }; + + // Ensure coin_out's denom is the same as the last swap operation's denom out + if coin_out.denom != last_op.denom_out { + return Err(ContractError::CoinOutDenomMismatch); + } + + // Get denom in from first swap operation to be used as the return coin's denom + let denom_in = first_op.denom_in.clone(); + + // Convert the swap operations to osmosis swap amount out routes + // Return an error if there was an error converting the swap + // operations to osmosis swap amount out routes. + let osmosis_swap_amount_out_routes: Vec = + convert_swap_operations(swap_operations).map_err(ContractError::ParseIntPoolID)?; + + // Query the osmosis poolmanager module to simulate the swap exact amount out + let res: EstimateSwapExactAmountOutResponse = PoolmanagerQuerier::new(&deps.querier) + .estimate_swap_exact_amount_out( + osmosis_swap_amount_out_routes.first().unwrap().pool_id, + osmosis_swap_amount_out_routes, + coin_out.to_string(), + )?; + + // Return the coin in needed + Ok(Coin { + denom: denom_in, + amount: Uint128::from_str(&res.token_in_amount)?, + }) +} diff --git a/contracts/networks/osmosis/swap/src/error.rs b/contracts/networks/osmosis/swap/src/error.rs new file mode 100644 index 00000000..8467b24d --- /dev/null +++ b/contracts/networks/osmosis/swap/src/error.rs @@ -0,0 +1,28 @@ +use cosmwasm_std::{OverflowError, StdError}; +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)] + Payment(#[from] cw_utils::PaymentError), + + #[error("Parse Int error raised: invalid pool String to pool id u64 conversion")] + ParseIntPoolID(#[from] std::num::ParseIntError), + + #[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, +} diff --git a/contracts/networks/osmosis/swap/src/lib.rs b/contracts/networks/osmosis/swap/src/lib.rs new file mode 100644 index 00000000..ed729875 --- /dev/null +++ b/contracts/networks/osmosis/swap/src/lib.rs @@ -0,0 +1,2 @@ +pub mod contract; +pub mod error; diff --git a/contracts/networks/osmosis/swap/tests/test_execute_swap.rs b/contracts/networks/osmosis/swap/tests/test_execute_swap.rs new file mode 100644 index 00000000..30bbdf6c --- /dev/null +++ b/contracts/networks/osmosis/swap/tests/test_execute_swap.rs @@ -0,0 +1,281 @@ +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env, mock_info}, + to_binary, Addr, Coin, + ReplyOn::Never, + SubMsg, WasmMsg, +}; +use osmosis_std::types::cosmos::base::v1beta1::Coin as OsmosisStdCoin; +use osmosis_std::types::osmosis::poolmanager::v1beta1::{MsgSwapExactAmountIn, SwapAmountInRoute}; +use skip::swap::{ExecuteMsg, SwapOperation}; +use skip_swap_osmosis_poolmanager_swap::error::ContractResult; +use test_case::test_case; + +/* +Test Cases: + +Expect Success + - One Swap Operation + - Multiple Swap Operations + - No Swap Operations (This is prevented in the entry point contract; and will fail on Osmosis module if attempted) + +Expect Error + - No Coin Sent + - More Than One Coin Sent + - Invalid Pool ID Conversion For Swap Operations + + */ + +// Define test parameters +struct Params { + info_funds: Vec, + swap_operations: Vec, + expected_messages: Vec, + expected_error_string: String, +} + +// Test execute_swap +#[test_case( + Params { + info_funds: vec![Coin::new(100, "uosmo")], + swap_operations: vec![ + SwapOperation { + pool: "1".to_string(), + denom_in: "uosmo".to_string(), + denom_out: "uatom".to_string(), + } + ], + expected_messages: vec![ + SubMsg { + id: 0, + msg: MsgSwapExactAmountIn { + sender: "swap_contract_address".to_string(), + routes: vec![ + SwapAmountInRoute { + pool_id: 1, + token_out_denom: "uatom".to_string(), + } + ], + token_in: Some( + OsmosisStdCoin { + denom: "uosmo".to_string(), + amount: "100".to_string(), + } + ), + token_out_min_amount: "1".to_string(), + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_contract_address".to_string(), + msg: to_binary(&ExecuteMsg::TransferFundsBack { + caller: Addr::unchecked("swapper"), + })?, + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error_string: "".to_string(), + }; +"One Swap Operation")] +#[test_case( + Params { + info_funds: vec![Coin::new(100, "uosmo")], + swap_operations: vec![ + SwapOperation { + pool: "1".to_string(), + denom_in: "uosmo".to_string(), + denom_out: "uatom".to_string(), + }, + SwapOperation { + pool: "2".to_string(), + denom_in: "uatom".to_string(), + denom_out: "untrn".to_string(), + } + ], + expected_messages: vec![ + SubMsg { + id: 0, + msg: MsgSwapExactAmountIn { + sender: "swap_contract_address".to_string(), + routes: vec![ + SwapAmountInRoute { + pool_id: 1, + token_out_denom: "uatom".to_string(), + }, + SwapAmountInRoute { + pool_id: 2, + token_out_denom: "untrn".to_string(), + } + ], + token_in: Some( + OsmosisStdCoin { + denom: "uosmo".to_string(), + amount: "100".to_string(), + } + ), + token_out_min_amount: "1".to_string(), + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_contract_address".to_string(), + msg: to_binary(&ExecuteMsg::TransferFundsBack { + caller: Addr::unchecked("swapper"), + })?, + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error_string: "".to_string(), + }; +"Multiple Swap Operations")] +#[test_case( + Params { + info_funds: vec![Coin::new(100, "uosmo")], + swap_operations: vec![], + expected_messages: vec![ + SubMsg { + id: 0, + msg: MsgSwapExactAmountIn { + sender: "swap_contract_address".to_string(), + routes: vec![], + token_in: Some( + OsmosisStdCoin { + denom: "uosmo".to_string(), + amount: "100".to_string(), + } + ), + token_out_min_amount: "1".to_string(), + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_contract_address".to_string(), + msg: to_binary(&ExecuteMsg::TransferFundsBack { + caller: Addr::unchecked("swapper"), + })?, + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error_string: "".to_string(), + }; +"No Swap Operations")] +#[test_case( + Params { + info_funds: vec![], + swap_operations: vec![ + SwapOperation { + pool: "pool_1".to_string(), + denom_in: "uosmo".to_string(), + denom_out: "uatom".to_string(), + } + ], + expected_messages: vec![], + expected_error_string: "No funds sent".to_string(), + }; + "No Coin Sent - Expect Error")] +#[test_case( + Params { + info_funds: vec![ + Coin::new(100, "uosmo"), + Coin::new(100, "uatom"), + ], + swap_operations: vec![ + SwapOperation { + pool: "pool_1".to_string(), + denom_in: "uosmo".to_string(), + denom_out: "uatom".to_string(), + } + ], + expected_messages: vec![], + expected_error_string: "Sent more than one denomination".to_string(), + }; + "More Than One Coin Sent - Expect Error")] +#[test_case( + Params { + info_funds: vec![Coin::new(100, "uosmo")], + swap_operations: vec![ + SwapOperation { + pool: "pool_1".to_string(), + denom_in: "uosmo".to_string(), + denom_out: "uatom".to_string(), + } + ], + expected_messages: vec![], + expected_error_string: "Parse Int error raised: invalid pool String to pool id u64 conversion".to_string(), + }; + "Invalid Pool ID Conversion For Swap Operations - Expect Error")] +fn test_execute_swap(params: Params) -> ContractResult<()> { + // Create mock dependencies + let mut deps = mock_dependencies(); + + // Create mock env + let mut env = mock_env(); + env.contract.address = Addr::unchecked("swap_contract_address"); + + // Convert info funds vector into a slice of Coin objects + let info_funds: &[Coin] = ¶ms.info_funds; + + // Create mock info with entry point contract address + let info = mock_info("swapper", info_funds); + + // Call execute_swap with the given test parameters + let res = skip_swap_osmosis_poolmanager_swap::contract::execute( + deps.as_mut(), + env, + info, + ExecuteMsg::Swap { + operations: params.swap_operations.clone(), + }, + ); + + // Assert the behavior is correct + match res { + Ok(res) => { + // Assert the test did not expect an error + assert!( + params.expected_error_string.is_empty(), + "expected test to error with {:?}, but it succeeded", + params.expected_error_string + ); + + // Assert the messages are correct + assert_eq!(res.messages, params.expected_messages); + } + Err(err) => { + // Assert the test expected an error + assert!( + !params.expected_error_string.is_empty(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err.to_string(), params.expected_error_string); + } + } + + Ok(()) +} diff --git a/contracts/networks/osmosis/swap/tests/test_execute_transfer_funds_back.rs b/contracts/networks/osmosis/swap/tests/test_execute_transfer_funds_back.rs new file mode 100644 index 00000000..2339817a --- /dev/null +++ b/contracts/networks/osmosis/swap/tests/test_execute_transfer_funds_back.rs @@ -0,0 +1,136 @@ +use cosmwasm_std::{ + testing::{mock_dependencies_with_balances, mock_env, mock_info}, + Addr, BankMsg, Coin, + ReplyOn::Never, + SubMsg, +}; +use skip::swap::ExecuteMsg; +use skip_swap_osmosis_poolmanager_swap::error::{ContractError, ContractResult}; +use test_case::test_case; + +/* +Test Cases: + +Expect Success + - One Coin Balance + - Multiple Coin Balance + - No Coin Balance (This will fail at the bank module if attempted) + */ + +// Define test parameters +struct Params { + contract_balance: Vec, + expected_messages: Vec, + expected_error: Option, +} + +// Test execute_swap +#[test_case( + Params { + contract_balance: vec![Coin::new(100, "uosmo")], + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "swapper".to_string(), + amount: vec![Coin::new(100, "uosmo")], + }.into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Transfers One Coin Balance")] +#[test_case( + Params { + contract_balance: vec![ + Coin::new(100, "uosmo"), + Coin::new(100, "uatom"), + ], + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "swapper".to_string(), + amount: vec![ + Coin::new(100, "uosmo"), + Coin::new(100, "uatom") + ], + }.into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Transfers Multiple Coin Balance")] +#[test_case( + Params { + contract_balance: vec![], + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "swapper".to_string(), + amount: vec![], + }.into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Transfers No Coin Balance")] +fn test_execute_swap(params: Params) -> ContractResult<()> { + // Convert params contract balance to a slice + let contract_balance: &[Coin] = ¶ms.contract_balance; + + // Create mock dependencies + let mut deps = mock_dependencies_with_balances(&[("swap_contract_address", contract_balance)]); + + // Create mock env + let mut env = mock_env(); + env.contract.address = Addr::unchecked("swap_contract_address"); + + // Create mock info + let info = mock_info("swap_contract_address", &[]); + + // Call execute_swap with the given test parameters + let res = skip_swap_osmosis_poolmanager_swap::contract::execute( + deps.as_mut(), + env, + info, + ExecuteMsg::TransferFundsBack { + caller: Addr::unchecked("swapper"), + }, + ); + + // Assert the behavior is correct + match res { + Ok(res) => { + // Assert the test did not expect an error + assert!( + params.expected_error.is_none(), + "expected test to error with {:?}, but it succeeded", + params.expected_error + ); + + // Assert the messages are correct + assert_eq!(res.messages, params.expected_messages); + } + Err(err) => { + // Assert the test expected an error + assert!( + params.expected_error.is_some(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err, params.expected_error.unwrap()); + } + } + + Ok(()) +} diff --git a/deployed-contracts/neutron/mainnet.toml b/deployed-contracts/neutron/mainnet.toml new file mode 100644 index 00000000..cdbd3c27 --- /dev/null +++ b/deployed-contracts/neutron/mainnet.toml @@ -0,0 +1,30 @@ +[info] +chain_id = "neutron-1" +network = "mainnet" +deploy_date = "28/06/2023 15:37:37" +commit_hash = "4c6211e7b70eee07f17bd1c8f3a000c5b0731b53" + +[checksums] +"skip_swap_entry_point-aarch64.wasm" = "599f3ebacaf7a56d6f703c4bd2a7ce2dc27e31f416fc35aa5b1f352c50985bb3" +"skip_swap_neutron_astroport_swap-aarch64.wasm" = "74287287bee3c3b8732de6571b7aadd50e9fadf10999be0faee73f53dd4f452d" +"skip_swap_neutron_ibc_transfer-aarch64.wasm" = "71de927903eb3073fd8e71e19a4570d547f3bc9c91c3182a55c4824e648dcc73" +"skip_swap_osmosis_ibc_transfer-aarch64.wasm" = "16ab3cbf84617b78bb21b408292d51d5cd68afac680e0b0dd2781e77e29152c0" +"skip_swap_osmosis_poolmanager_swap-aarch64.wasm" = "b6752f85ec262b6f6b60e0b3c501a167a0b2337efc8aed06692ad02ffe64ebdc" + +[code-ids] +swap_adapter_contract_code_id = 63 +ibc_transfer_adapter_contract_code_id = 64 +entry_point_contract_code_id = 65 + +[contract-addresses] +swap_adapter_contract_address = "neutron1khswzgxzjfeqesff3mk0ghs7m6904e70ddfhn6n3evhu5jadg58s7pve5u" +ibc_transfer_adapter_contract_address = "neutron1kuu469eu03v7dsgaptrcnhl8q8jqh5rta006lzlgyxxlxmsp3gaq3vleht" +entry_point_contract_address = "neutron1grhx4mkt04aay5vnufyxxc98q44va72c80dgxdwu9dv29sxvq3usv9z8yv" + +[tx-hashes] +store_swap_adapter_tx_hash = "21D205FBFF53C7DE772B719AA813ECCC595495CD2CBDFEBAD81563143036267D" +store_ibc_transfer_adapter_tx_hash = "B27BC47AB9A054B428EECF02E9810B6454D72FEFE5F9473FB3323561FC61C6E2" +store_entry_point_tx_hash = "069BB89A1FCD8E51B0BF700E32B49CB5B44D0EBBB3E6BB82D474D4BA1688A6BC" +instantiate_swap_adapter_tx_hash = "75D9557E6439B6DC0D957FE8D7410BC0C83A2BD4498C4EC7321AA67AC725DCB5" +instantiate_ibc_transfer_adapter_tx_hash = "BD53B83506B5E335C06540308273A19F1C1A2678F5DE452B8C41209C44293D9B" +instantiate_entry_point_tx_hash = "A950B3CAD0B1ED1EAB3CDF0B37D211F8EB1F3014BA62D569AC5137B7D1620CAF" diff --git a/deployed-contracts/neutron/testnet.toml b/deployed-contracts/neutron/testnet.toml new file mode 100644 index 00000000..b8ff0484 --- /dev/null +++ b/deployed-contracts/neutron/testnet.toml @@ -0,0 +1,18 @@ +# Chain ID +chain_id = "pion-1" + +# Deployment date +deployment_date = "2023-06-20" + +# Repo commit hash +repo_commit_hash = "211833bc821e93216e1fedc7fd8ee8b2899932b4" + +# Deployment checksums +entry_point_contract_checksum = "b6c516e3d7001d6e5bafe6b13ef980884c951efcf44dbf8ec93e2f1c3a1f3089" +astroport_swap_adapter_checksum = "a9ba9e6a9c01d63f172ed05fd0d72283bac097423f394119d2a097d4b5ea9715" +ibc_transfer_adapter_checksum = "74b33690465bb3e0ef46ad8017e4263c962d213ebeb522529bc9e9aec004b0c3" + +# Deployed contracts addresses +entry_point_contract_address = "neutron1a0f2kt344udfry93s9jflvjfjpl84fmxxzhhku565hpapaclz7jqg5vmr6" +astroport_swap_adapter_contract_address = "neutron1yqxqqv89vdkr4lxd4rt4qkkcr50u655caylvexgfnvjggex8f0eqtgr29s" +ibc_transfer_adapter_contract_address = "neutron1sdxzpn5rjzgjcxg6u5vhka9gwvzq05lfchx5pkx4nle6znx4z9cq8wtf9q" \ No newline at end of file diff --git a/deployed-contracts/osmosis/testnet.toml b/deployed-contracts/osmosis/testnet.toml new file mode 100644 index 00000000..6b37efc1 --- /dev/null +++ b/deployed-contracts/osmosis/testnet.toml @@ -0,0 +1,30 @@ +[info] +chain_id = "osmo-test-5" +network = "testnet" +deploy_date = "28/06/2023 15:31:41" +commit_hash = "4c6211e7b70eee07f17bd1c8f3a000c5b0731b53" + +[checksums] +"skip_swap_entry_point-aarch64.wasm" = "599f3ebacaf7a56d6f703c4bd2a7ce2dc27e31f416fc35aa5b1f352c50985bb3" +"skip_swap_neutron_astroport_swap-aarch64.wasm" = "74287287bee3c3b8732de6571b7aadd50e9fadf10999be0faee73f53dd4f452d" +"skip_swap_neutron_ibc_transfer-aarch64.wasm" = "71de927903eb3073fd8e71e19a4570d547f3bc9c91c3182a55c4824e648dcc73" +"skip_swap_osmosis_ibc_transfer-aarch64.wasm" = "16ab3cbf84617b78bb21b408292d51d5cd68afac680e0b0dd2781e77e29152c0" +"skip_swap_osmosis_poolmanager_swap-aarch64.wasm" = "b6752f85ec262b6f6b60e0b3c501a167a0b2337efc8aed06692ad02ffe64ebdc" + +[code-ids] +swap_adapter_contract_code_id = 1392 +ibc_transfer_adapter_contract_code_id = 1393 +entry_point_contract_code_id = 1394 + +[contract-addresses] +swap_adapter_contract_address = "osmo1ttv6e7mv3hwsy8u7erzfxhvfx55nxz5w422705jht6xk6hly745sj6aala" +ibc_transfer_adapter_contract_address = "osmo16yn7gmgmsq55tlvly27tqnyrz2jry8m720k34w88jrfr0nhpgmhq4vec03" +entry_point_contract_address = "osmo1hjzln3e58qtfljce4g3c9e9x7p8wygvlu55d9edcwukpd2uww4ssqqg7m6" + +[tx-hashes] +store_swap_adapter_tx_hash = "C93124E834B2C4375291E94A00D4878EC44179547436722F4A265C330ED70EB4" +store_ibc_transfer_adapter_tx_hash = "A3FB27B49FBDFA36D9A610F1AB4178FFDE5E6D42DB6BC221899E57B14D4C251C" +store_entry_point_tx_hash = "5245009F303710198C82140E6AB73AE14849FC367C2FAD49996976D6254D1143" +instantiate_swap_adapter_tx_hash = "9E5D552A7D3DB1E94EFEA7C3FC058A496B9AB745D38B98566995121C925BB4AD" +instantiate_ibc_transfer_adapter_tx_hash = "FC33E4C08498DF8DBB6608838A692C163AE8C4892F1A72DF9374517EE2474262" +instantiate_entry_point_tx_hash = "C3CDF6E9B47E18FBE14196022764AA351AC39C0FEEA06904BC77713502AB8AC3" diff --git a/packages/skip/Cargo.toml b/packages/skip/Cargo.toml new file mode 100644 index 00000000..e1077e37 --- /dev/null +++ b/packages/skip/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "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 } +ibc-proto = { workspace = true } +neutron-proto = { workspace = true } +osmosis-std = { workspace = true } diff --git a/packages/skip/src/entry_point.rs b/packages/skip/src/entry_point.rs new file mode 100644 index 00000000..194a752f --- /dev/null +++ b/packages/skip/src/entry_point.rs @@ -0,0 +1,81 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Binary, Coin, Uint128}; + +use crate::{ + ibc::IbcInfo, + swap::{SwapExactCoinIn, SwapExactCoinOut, SwapVenue}, +}; + +/////////////////// +/// INSTANTIATE /// +/////////////////// + +#[cw_serde] +pub struct InstantiateMsg { + pub swap_venues: Vec, + pub ibc_transfer_contract_address: String, +} + +/////////////// +/// EXECUTE /// +/////////////// + +#[cw_serde] +pub enum ExecuteMsg { + SwapAndAction { + fee_swap: Option, + user_swap: SwapExactCoinIn, + min_coin: Coin, + timeout_timestamp: u64, + post_swap_action: PostSwapAction, + affiliates: Vec, + }, + PostSwapAction { + min_coin: Coin, + timeout_timestamp: u64, + post_swap_action: PostSwapAction, + affiliates: Vec, + }, +} + +///////////// +/// QUERY /// +///////////// + +#[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 /// +//////////////////// + +#[cw_serde] +pub enum PostSwapAction { + BankSend { + to_address: String, + }, + IbcTransfer { + ibc_info: IbcInfo, + }, + ContractCall { + contract_address: String, + msg: Binary, + }, +} + +#[cw_serde] +pub struct Affiliate { + pub basis_points_fee: Uint128, + pub address: String, +} diff --git a/packages/skip/src/ibc.rs b/packages/skip/src/ibc.rs new file mode 100644 index 00000000..8d0e48e1 --- /dev/null +++ b/packages/skip/src/ibc.rs @@ -0,0 +1,214 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Coin, OverflowError, Uint128}; +use std::convert::From; + +use std::collections::BTreeMap; + +use crate::proto_coin::ProtoCoin; +use neutron_proto::neutron::feerefunder::Fee as NeutronFee; + +/////////////////// +/// INSTANTIATE /// +/////////////////// + +#[cw_serde] +pub struct InstantiateMsg {} + +/////////////// +/// EXECUTE /// +/////////////// + +#[cw_serde] +pub enum ExecuteMsg { + IbcTransfer { + info: IbcInfo, + coin: Coin, + timeout_timestamp: u64, + }, +} + +///////////// +/// QUERY /// +///////////// + +#[cw_serde] +#[derive(QueryResponses)] +pub enum NeutronQueryMsg { + #[returns(NeutronInProgressIbcTransfer)] + InProgressIbcTransfer { + channel_id: String, + sequence_id: u64, + }, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum OsmosisQueryMsg { + #[returns(OsmosisInProgressIbcTransfer)] + InProgressIbcTransfer { + channel_id: String, + sequence_id: u64, + }, +} + +//////////////////// +/// COMMON TYPES /// +//////////////////// + +#[cw_serde] +pub struct IbcFee { + pub recv_fee: Vec, + pub ack_fee: Vec, + pub timeout_fee: Vec, +} + +// IbcFeeMap is a type alias for a BTreeMap of String denom to Uint128 total amount +pub struct IbcFeeMap(pub BTreeMap); + +// Converts an IbcFee struct to a BTreeMap of String denom to Uint128 total amount +impl TryFrom for IbcFeeMap { + type Error = OverflowError; + + fn try_from(ibc_fee: IbcFee) -> Result { + let mut ibc_fees: IbcFeeMap = IbcFeeMap(BTreeMap::new()); + + for coin in [ibc_fee.recv_fee, ibc_fee.ack_fee, ibc_fee.timeout_fee] + .iter() + .flatten() + { + ibc_fees.add_coin(coin)?; + } + + Ok(ibc_fees) + } +} + +// Implement add coin and get amount methods for IbcFeeMap +impl IbcFeeMap { + // Takes a coin and adds it to the IbcFeeMap + pub fn add_coin(&mut self, coin: &Coin) -> Result<(), OverflowError> { + let amount = self + .0 + .entry(coin.denom.clone()) + .or_insert_with(Uint128::zero); + *amount = amount.checked_add(coin.amount)?; + + Ok(()) + } + + // Given a denom, returns the total amount of that denom in the IbcFeeMap + // or returns 0 if the denom is not in the IbcFeeMap. + pub fn get_amount(&self, denom: &str) -> Uint128 { + self.0.get(denom).cloned().unwrap_or_default() + } +} + +// Converts an IbcFeeMap to a Vec +impl From for Vec { + fn from(ibc_fee_map: IbcFeeMap) -> Self { + ibc_fee_map + .0 + .into_iter() + .map(|(denom, amount)| Coin { denom, amount }) + .collect() + } +} + +// Converts an IbcFee struct to a neutron_proto 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(), + } + } +} + +#[cw_serde] +pub struct IbcInfo { + pub source_channel: String, + pub receiver: String, + pub fee: IbcFee, + pub memo: String, + pub recover_address: String, +} + +#[cw_serde] +pub struct IbcTransfer { + pub info: IbcInfo, + pub coin: Coin, + pub timeout_timestamp: u64, +} + +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); + +// NeutronInProgressIBCTransfer is a struct that is used to store the ibc transfer information +// upon receiving a successful response from the neutron ibc transfer sub message. Later +// to be used in the sudo handler to send the coin back to the recover address if the +// ibc transfer packet acknowledgement is an error or times out. Also used to send the +// user back the refunded ack fee or timeout fee based on the type of acknowledgement +#[cw_serde] +pub struct NeutronInProgressIbcTransfer { + pub recover_address: String, + pub coin: Coin, + pub ack_fee: Vec, + pub timeout_fee: Vec, +} + +// OsmosisInProgressIBCTransfer is a struct that is used to store the ibc transfer information +// upon receiving a successful sub message reply. Later +// to be used in the sudo handler to send the coin back to the recover address if the +// ibc transfer packet acknowledgement is an error or times out. Also used to send the +// user back the refunded ack fee or timeout fee based on the type of acknowledgement +#[cw_serde] +pub struct OsmosisInProgressIbcTransfer { + pub recover_address: String, + pub coin: Coin, + pub channel_id: String, +} + +#[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, + }, +} diff --git a/packages/skip/src/lib.rs b/packages/skip/src/lib.rs new file mode 100644 index 00000000..8e050fae --- /dev/null +++ b/packages/skip/src/lib.rs @@ -0,0 +1,5 @@ +pub mod entry_point; +pub mod ibc; +pub mod proto_coin; +pub mod sudo; +pub mod swap; diff --git a/packages/skip/src/proto_coin.rs b/packages/skip/src/proto_coin.rs new file mode 100644 index 00000000..f2632579 --- /dev/null +++ b/packages/skip/src/proto_coin.rs @@ -0,0 +1,43 @@ +use cosmwasm_schema::cw_serde; + +use cosmos_sdk_proto::cosmos::base::v1beta1::Coin as CosmosSdkCoin; +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(), + } + } +} diff --git a/packages/skip/src/sudo.rs b/packages/skip/src/sudo.rs new file mode 100644 index 00000000..8f2e7d0c --- /dev/null +++ b/packages/skip/src/sudo.rs @@ -0,0 +1,26 @@ +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/skip/src/swap.rs b/packages/skip/src/swap.rs new file mode 100644 index 00000000..615fd15d --- /dev/null +++ b/packages/skip/src/swap.rs @@ -0,0 +1,170 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Coin}; +use std::{ + convert::{From, TryFrom}, + num::ParseIntError, +}; + +use astroport::{asset::AssetInfo, router::SwapOperation as AstroportSwapOperation}; + +use osmosis_std::types::osmosis::poolmanager::v1beta1::{ + SwapAmountInRoute as OsmosisSwapAmountInRoute, SwapAmountOutRoute as OsmosisSwapAmountOutRoute, +}; + +/////////////////// +/// INSTANTIATE /// +/////////////////// + +#[cw_serde] +pub struct OsmosisInstantiateMsg {} + +#[cw_serde] +pub struct NeutronInstantiateMsg { + pub router_contract_address: String, +} + +///////////////////////// +/// EXECUTE /// +///////////////////////// + +#[cw_serde] +pub enum ExecuteMsg { + Swap { operations: Vec }, + TransferFundsBack { caller: Addr }, +} + +impl From for ExecuteMsg { + fn from(swap: SwapExactCoinIn) -> Self { + ExecuteMsg::Swap { + operations: swap.operations, + } + } +} + +impl From for ExecuteMsg { + fn from(swap: SwapExactCoinOut) -> Self { + ExecuteMsg::Swap { + operations: swap.operations, + } + } +} + +///////////////////////// +/// QUERY /// +///////////////////////// + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + // RouterContractAddress returns the address of the router contract + #[returns(Addr)] + RouterContractAddress {}, + // SimulateSwapExactAmountOut returns the coin in necessary to receive the specified coin out + #[returns(Coin)] + SimulateSwapExactCoinOut { + coin_out: Coin, + swap_operations: Vec, + }, + // SimulateSwapExactAmountIn returns the coin out received from the specified coin in + #[returns(Coin)] + SimulateSwapExactCoinIn { + coin_in: Coin, + swap_operations: Vec, + }, +} + +//////////////////// +/// 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_address: String, +} + +// 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, +} + +// ASTROPORT CONVERSIONS + +// Converts a skip swap operation to an astroport swap operation +impl From for AstroportSwapOperation { + fn from(swap_operation: SwapOperation) -> Self { + // Convert the swap operation to an astroport swap operation and return it + AstroportSwapOperation::AstroSwap { + offer_asset_info: AssetInfo::NativeToken { + denom: swap_operation.denom_in, + }, + ask_asset_info: AssetInfo::NativeToken { + denom: swap_operation.denom_out, + }, + } + } +} + +// OSMOSIS CONVERSIONS + +// Converts a skip swap operation to an osmosis swap amount in route +// Error if the given String for pool in the swap operation is not a valid u64. +impl TryFrom for OsmosisSwapAmountInRoute { + type Error = ParseIntError; + + fn try_from(swap_operation: SwapOperation) -> Result { + Ok(OsmosisSwapAmountInRoute { + pool_id: swap_operation.pool.parse()?, + token_out_denom: swap_operation.denom_out, + }) + } +} + +// Converts a skip swap operation to an osmosis swap amount out route +// Error if the given String for pool in the swap operation is not a valid u64. +impl TryFrom for OsmosisSwapAmountOutRoute { + type Error = ParseIntError; + + fn try_from(swap_operation: SwapOperation) -> Result { + Ok(OsmosisSwapAmountOutRoute { + pool_id: swap_operation.pool.parse()?, + token_in_denom: swap_operation.denom_in, + }) + } +} + +// 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 coin with the given vector of swap operations +#[cw_serde] +pub struct SwapExactCoinOut { + pub swap_venue_name: String, + pub coin_out: Coin, + pub operations: Vec, +} + +// Swap object that swaps the given coin in when present. When not present, +// swaps the remaining coin recevied from the contract call minus fee swap (if present) +#[cw_serde] +pub struct SwapExactCoinIn { + pub swap_venue_name: String, + pub coin_in: Option, + pub operations: Vec, +} diff --git a/scripts/configs/neutron.toml b/scripts/configs/neutron.toml new file mode 100644 index 00000000..ffbb6c89 --- /dev/null +++ b/scripts/configs/neutron.toml @@ -0,0 +1,24 @@ +# Enter your mnemonic here +MNEMONIC = "" + +# Commut Hash +COMMIT_HASH = "4c6211e7b70eee07f17bd1c8f3a000c5b0731b53" + +MAINNET_REST_URL = "rest+https://neutron-api.lavenderfive.com" +MAINNET_CHAIN_ID = "neutron-1" + +TESTNET_REST_URL = "rest+https://rest-palvus.pion-1.ntrn.tech" +TESTNET_CHAIN_ID = "pion-1" + +ADDRESS_PREFIX = "neutron" +DENOM = "untrn" +GAS_PRICE = 0.01 + +# Contract Paths +ENTRY_POINT_CONTRACT_PATH = "../artifacts/skip_swap_entry_point-aarch64.wasm" +SWAP_ADAPTER_PATH = "../artifacts/skip_swap_neutron_astroport_swap-aarch64.wasm" +IBC_TRANSFER_ADAPTER_PATH = "../artifacts/skip_swap_neutron_ibc_transfer-aarch64.wasm" + +[[swap_venues]] +name = "neutron-astroport" +router_contract_address = "neutron1eeyntmsq448c68ez06jsy6h2mtjke5tpuplnwtjfwcdznqmw72kswnlmm0" \ No newline at end of file diff --git a/scripts/configs/osmosis.toml b/scripts/configs/osmosis.toml new file mode 100644 index 00000000..8bf59666 --- /dev/null +++ b/scripts/configs/osmosis.toml @@ -0,0 +1,23 @@ +# Enter your mnemonic here +MNEMONIC = "" + +# Commut Hash +COMMIT_HASH = "4c6211e7b70eee07f17bd1c8f3a000c5b0731b53" + +MAINNET_REST_URL = "rest+https://lcd.osmosis.zone/" +MAINNET_CHAIN_ID = "osmosis-1" + +TESTNET_REST_URL = "rest+https://lcd.osmotest5.osmosis.zone/" +TESTNET_CHAIN_ID = "osmo-test-5" + +ADDRESS_PREFIX = "osmo" +DENOM = "uosmo" +GAS_PRICE = 0.01 + +# Contract Paths +ENTRY_POINT_CONTRACT_PATH = "../artifacts/skip_swap_entry_point-aarch64.wasm" +SWAP_ADAPTER_PATH = "../artifacts/skip_swap_osmosis_poolmanager_swap-aarch64.wasm" +IBC_TRANSFER_ADAPTER_PATH = "../artifacts/skip_swap_osmosis_ibc_transfer-aarch64.wasm" + +[[swap_venues]] +name = "osmosis-poolmanager" \ No newline at end of file diff --git a/scripts/deploy.py b/scripts/deploy.py new file mode 100644 index 00000000..fd09d440 --- /dev/null +++ b/scripts/deploy.py @@ -0,0 +1,283 @@ +from bip_utils import Bip39SeedGenerator, Bip44, Bip44Coins + +from cosmpy.aerial.client import LedgerClient, NetworkConfig +from cosmpy.aerial.contract import LedgerContract, create_cosmwasm_execute_msg, create_cosmwasm_instantiate_msg +from cosmpy.aerial.tx import Transaction, SigningCfg +from cosmpy.aerial.wallet import LocalWallet +from cosmpy.crypto.keypairs import PrivateKey +from cosmpy.protos.cosmos.base.v1beta1.coin_pb2 import Coin +from cosmpy.protos.cosmwasm.wasm.v1.tx_pb2 import MsgStoreCode + +import os +import sys +import toml +from datetime import datetime + +CHAIN = sys.argv[1] +NETWORK = sys.argv[2] +DEPLOYED_CONTRACTS_FOLDER_PATH = "../deployed-contracts" + +# Match the CHAIN to the file name in the configs folder +found_config = False +for file in os.listdir("configs"): + if file == f"{CHAIN}.toml": + config = toml.load(f"configs/{file}") + found_config = True + break + +# Raise exception if config not found +if not found_config: + raise Exception(f"Could not find config for chain {CHAIN}; Must enter a chain as 1st command line argument.") + +# Create deployed-contracts folder if it doesn't exist +if not os.path.exists(f"../deployed-contracts"): + os.makedirs(f"../deployed-contracts") + +# Create chain folder if it doesn't exist within deployed-contracts +if not os.path.exists(f"../deployed-contracts/{CHAIN}"): + os.makedirs(f"../deployed-contracts/{CHAIN}") + +# Choose network to deploy to based on cli args +if NETWORK == "mainnet": + REST_URL = config["MAINNET_REST_URL"] + CHAIN_ID = config["MAINNET_CHAIN_ID"] +elif NETWORK == "testnet": + REST_URL = config["TESTNET_REST_URL"] + CHAIN_ID = config["TESTNET_CHAIN_ID"] +else: + raise Exception("Must specify either 'mainnet' or 'testnet' for 2nd command line argument.") + +ADDRESS_PREFIX = config["ADDRESS_PREFIX"] +DENOM = config["DENOM"] +GAS_PRICE = config["GAS_PRICE"] + +# Contract Paths +ENTRY_POINT_CONTRACT_PATH = config["ENTRY_POINT_CONTRACT_PATH"] +SWAP_ADAPTER_PATH = config["SWAP_ADAPTER_PATH"] +IBC_TRANSFER_ADAPTER_PATH = config["IBC_TRANSFER_ADAPTER_PATH"] + +MNEMONIC = config["MNEMONIC"] +del config["MNEMONIC"] + +DEPLOYED_CONTRACTS_INFO = {} + +def main(): + # Create network config and client + cfg = NetworkConfig( + chain_id=CHAIN_ID, + url=REST_URL, + fee_minimum_gas_price=.01, + fee_denomination=DENOM, + staking_denomination=DENOM, + ) + client = LedgerClient(cfg) + + # Create wallet from mnemonic + wallet = create_wallet(client) + + # Initialize deployed contracts info + init_deployed_contracts_info() + + # Get checksums for deployed contracts info + with open("../artifacts/checksums.txt", "r") as f: + checksums = f.read().split() + + # Store checksums for deployed contracts info + for i in range(0, len(checksums), 2): + DEPLOYED_CONTRACTS_INFO["checksums"][checksums[i+1]] = checksums[i] + with open(f"{DEPLOYED_CONTRACTS_FOLDER_PATH}/{CHAIN}/{NETWORK}.toml", "w") as f: + toml.dump(DEPLOYED_CONTRACTS_INFO, f) + + # Store contracts + swap_adapter_contract_code_id = store_contract(client, wallet, SWAP_ADAPTER_PATH, "swap_adapter") + ibc_transfer_adapter_contract_code_id = store_contract(client, wallet, IBC_TRANSFER_ADAPTER_PATH, "ibc_transfer_adapter") + entry_point_contract_code_id = store_contract(client, wallet, ENTRY_POINT_CONTRACT_PATH, "entry_point") + + # Intantiate contracts + if "router_contract_address" in config["swap_venues"][0]: + swap_adapter_args = {"router_contract_address": f"{config['swap_venues'][0]['router_contract_address']}"} + else: + swap_adapter_args = {} + swap_adapter_contract_address = instantiate_contract( + client, + wallet, + swap_adapter_contract_code_id, + swap_adapter_args, + "Skip Swap Swap Adapter", + "swap_adapter" + ) + ibc_transfer_adapter_contract_address = instantiate_contract( + client, + wallet, + ibc_transfer_adapter_contract_code_id, + {}, + "Skip Swap IBC Transfer Adapter", + "ibc_transfer_adapter" + ) + entry_point_contract_address = instantiate_contract( + client=client, + wallet=wallet, + code_id=entry_point_contract_code_id, + args={ + "swap_venues": [ + { + "name": config["swap_venues"][0]["name"], + "adapter_contract_address": swap_adapter_contract_address, + } + ], + "ibc_transfer_contract_address": ibc_transfer_adapter_contract_address, + }, + label="Skip Swap Entry Point", + name="entry_point" + ) + +def create_tx(msg, + client, + wallet, + gas_limit: int, + fee: str, + ) -> tuple[bytes, str]: + tx = Transaction() + tx.add_message(msg) + + # Get account + account = client.query_account(str(wallet.address())) + + # Seal, Sign, and Complete Tx + tx.seal(signing_cfgs=[SigningCfg.direct(wallet.public_key(), account.sequence)], fee = fee, gas_limit=gas_limit) + tx.sign(wallet.signer(), client.network_config.chain_id, account.number) + tx.complete() + + return tx + +def create_wasm_store_tx(client, + wallet, + address: str, + gas_fee: str, + gas_limit: int, + file: str, + ) -> tuple[bytes, str]: + msg = MsgStoreCode( + sender=address, + wasm_byte_code=open(file, "rb").read(), + instantiate_permission=None + ) + return create_tx(msg=msg, + client=client, + wallet=wallet, + gas_limit=gas_limit, + fee=gas_fee) + +def create_wasm_instantiate_tx( + client, + wallet, + address: str, + gas_fee: str, + gas_limit: int, + code_id: int, + args: dict, + label: str, + ) -> tuple[bytes, str]: + msg = create_cosmwasm_instantiate_msg( + code_id=code_id, + args=args, + label=label, + sender_address=address, + ) + return create_tx(msg=msg, + client=client, + wallet=wallet, + gas_limit=gas_limit, + fee=gas_fee) + +def create_wasm_execute_tx( + client, + wallet, + contract_address: str, + args: dict, + address: str, + gas_fee: str, + gas_limit: int, + funds_coin: Coin | None, + ) -> tuple[bytes, str]: + msg = create_cosmwasm_execute_msg( + contract_address=contract_address, + args=args, + sender_address=address + ) + if funds_coin: + msg.funds.append(funds_coin) + return create_tx(msg=msg, + client=client, + wallet=wallet, + gas_limit=gas_limit, + fee=gas_fee) + +def create_wallet(client) -> LocalWallet: + """ Create a wallet from a mnemonic and return it""" + seed_bytes = Bip39SeedGenerator(MNEMONIC).Generate() + bip44_def_ctx = Bip44.FromSeed(seed_bytes, Bip44Coins.COSMOS).DeriveDefaultPath() + wallet = LocalWallet(PrivateKey(bip44_def_ctx.PrivateKey().Raw().ToBytes()), prefix=ADDRESS_PREFIX) + balance = client.query_bank_balance(str(wallet.address()), DENOM) + print("Wallet Address: ", wallet.address(), " with account balance: ", balance) + return wallet + +def init_deployed_contracts_info(): + DEPLOYED_CONTRACTS_INFO["info"] = {} + DEPLOYED_CONTRACTS_INFO["info"]["chain_id"] = CHAIN_ID + DEPLOYED_CONTRACTS_INFO["info"]["network"] = NETWORK + DEPLOYED_CONTRACTS_INFO["info"]["deploy_date"] = datetime.now().strftime("%d/%m/%Y %H:%M:%S") + DEPLOYED_CONTRACTS_INFO["info"]["commit_hash"] = config["COMMIT_HASH"] + DEPLOYED_CONTRACTS_INFO["checksums"] = {} + DEPLOYED_CONTRACTS_INFO["code-ids"] = {} + DEPLOYED_CONTRACTS_INFO["contract-addresses"] = {} + DEPLOYED_CONTRACTS_INFO["tx-hashes"] = {} + with open(f"{DEPLOYED_CONTRACTS_FOLDER_PATH}/{CHAIN}/{NETWORK}.toml", "w") as f: + toml.dump(DEPLOYED_CONTRACTS_INFO, f) + +def store_contract(client, wallet, file_path, name) -> int: + gas_limit = 3000000 + store_ibc_adapter_tx = create_wasm_store_tx( + client=client, + wallet=wallet, + address=str(wallet.address()), + gas_fee=f"{int(GAS_PRICE*gas_limit)}{DENOM}", + gas_limit=gas_limit, + file=file_path, + ) + submitted_tx = client.broadcast_tx(store_ibc_adapter_tx) + print("Tx hash: ", submitted_tx.tx_hash) + submitted_tx.wait_to_complete(timeout=60) + contract_code_id = submitted_tx.contract_code_id + print(f"Skip Swap {name} Contract Code ID:", submitted_tx.contract_code_id) + DEPLOYED_CONTRACTS_INFO["code-ids"][f"{name}_contract_code_id"] = contract_code_id + DEPLOYED_CONTRACTS_INFO["tx-hashes"][f"store_{name}_tx_hash"] = submitted_tx.tx_hash + with open(f"{DEPLOYED_CONTRACTS_FOLDER_PATH}/{CHAIN}/{NETWORK}.toml", "w") as f: + toml.dump(DEPLOYED_CONTRACTS_INFO, f) + return int(contract_code_id) + +def instantiate_contract(client, wallet, code_id, args, label, name) -> str: + gas_limit = 200000 + instantiate_swap_adapter_tx = create_wasm_instantiate_tx( + client=client, + wallet=wallet, + address=str(wallet.address()), + gas_fee=f"{int(GAS_PRICE*gas_limit)}{DENOM}", + gas_limit=gas_limit, + code_id=code_id, + args=args, + label=label + ) + submitted_tx = client.broadcast_tx(instantiate_swap_adapter_tx) + print("Tx hash: ", submitted_tx.tx_hash) + submitted_tx.wait_to_complete(timeout=60) + contract_address = submitted_tx.contract_address.__str__() + print(f"Skip Swap {name} Contract Address:", contract_address) + DEPLOYED_CONTRACTS_INFO["contract-addresses"][f"{name}_contract_address"] = contract_address + DEPLOYED_CONTRACTS_INFO["tx-hashes"][f"instantiate_{name}_tx_hash"] = submitted_tx.tx_hash + with open(f"{DEPLOYED_CONTRACTS_FOLDER_PATH}/{CHAIN}/{NETWORK}.toml", "w") as f: + toml.dump(DEPLOYED_CONTRACTS_INFO, f) + return contract_address + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 00000000..ef7b41be --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1,69 @@ +appnope==0.1.3 +asn1crypto==1.5.1 +asttokens==2.2.1 +attrs==23.1.0 +backcall==0.2.0 +bech32==1.2.0 +bip-utils==2.7.1 +blspy==2.0.2 +cachetools==5.3.1 +cbor2==5.4.6 +certifi==2023.5.7 +cffi==1.15.1 +charset-normalizer==3.1.0 +coincurve==17.0.0 +comm==0.1.3 +cosmpy==0.8.0 +crcmod==1.7 +debugpy==1.6.7 +decorator==5.1.1 +ecdsa==0.18.0 +ed25519-blake2b==1.4 +executing==1.2.0 +google-api-core==2.11.1 +google-api-python-client==2.90.0 +google-auth==2.21.0 +google-auth-httplib2==0.1.0 +googleapis-common-protos==1.59.1 +grpcio==1.51.1 +httplib2==0.22.0 +idna==3.4 +ipykernel==6.23.3 +ipython==8.14.0 +jedi==0.18.2 +jsonschema==4.17.3 +jupyter_client==8.3.0 +jupyter_core==5.3.1 +matplotlib-inline==0.1.6 +nest-asyncio==1.5.6 +packaging==23.1 +parso==0.8.3 +pexpect==4.8.0 +pickleshare==0.7.5 +platformdirs==3.8.0 +prompt-toolkit==3.0.38 +protobuf==4.23.3 +psutil==5.9.5 +ptyprocess==0.7.0 +pure-eval==0.2.2 +py-sr25519-bindings==0.2.0 +pyasn1==0.5.0 +pyasn1-modules==0.3.0 +pycparser==2.21 +pycryptodome==3.18.0 +Pygments==2.15.1 +PyNaCl==1.5.0 +pyparsing==3.1.0 +pyrsistent==0.19.3 +python-dateutil==2.8.2 +pyzmq==25.1.0 +requests==2.31.0 +rsa==4.9 +six==1.16.0 +stack-data==0.6.2 +toml==0.10.2 +tornado==6.3.2 +traitlets==5.9.0 +uritemplate==4.1.1 +urllib3==1.26.16 +wcwidth==0.2.6 diff --git a/skip_swirl.png b/skip_swirl.png new file mode 100644 index 00000000..7fda9ba2 Binary files /dev/null and b/skip_swirl.png differ