diff --git a/.github/workflows/interchaintest.yml b/.github/workflows/interchaintest.yml index 2c9c2371..e37d8694 100644 --- a/.github/workflows/interchaintest.yml +++ b/.github/workflows/interchaintest.yml @@ -83,6 +83,29 @@ jobs: - name: swap covenant run: mkdir interchaintest/swap/wasms && cp -R artifacts/*.wasm interchaintest/swap/wasms && just local-e2e swap TestTokenSwap + single-party-pol-covenant: + needs: compile-contracts + runs-on: ubuntu-latest + steps: + - name: checkout repository + uses: actions/checkout@v3 + + - uses: actions/download-artifact@v3 + with: + name: contracts + path: artifacts/ + + - name: Set up Go ${{ env.GO_VERSION }} + uses: actions/setup-go@v4 + with: + go-version: ${{ env.GO_VERSION }} + + - name: setup just + uses: extractions/setup-just@v1 + + - name: single party pol covenant + run: mkdir interchaintest/single-party-pol/wasms && cp -R artifacts/*.wasm interchaintest/single-party-pol/wasms && cp -R interchaintest/wasms/astroport/*.wasm interchaintest/single-party-pol/wasms && just local-e2e single-party-pol + two-party-pol-covenant: needs: compile-contracts diff --git a/Cargo.lock b/Cargo.lock index fe552401..f7cc8f54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,9 +15,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "astroport" @@ -48,6 +48,33 @@ dependencies = [ "uint", ] +[[package]] +name = "astroport" +version = "3.8.0" +source = "git+https://github.com/astroport-fi/astroport-core.git#80595f1e7a4967e3d606efef2111d0ac67747f9f" +dependencies = [ + "astroport-circular-buffer", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.15.1", + "cw-utils 1.0.3", + "cw20 0.15.1", + "cw3", + "itertools 0.10.5", + "uint", +] + +[[package]] +name = "astroport-circular-buffer" +version = "0.1.0" +source = "git+https://github.com/astroport-fi/astroport-core.git#80595f1e7a4967e3d606efef2111d0ac67747f9f" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.15.1", + "thiserror", +] + [[package]] name = "astroport-factory" version = "1.5.1" @@ -63,6 +90,22 @@ dependencies = [ "thiserror", ] +[[package]] +name = "astroport-factory" +version = "1.6.0" +source = "git+https://github.com/astroport-fi/astroport-core.git#80595f1e7a4967e3d606efef2111d0ac67747f9f" +dependencies = [ + "astroport 3.8.0", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.15.1", + "cw-utils 1.0.3", + "cw2 0.15.1", + "itertools 0.10.5", + "protobuf 2.28.0", + "thiserror", +] + [[package]] name = "astroport-native-coin-registry" version = "1.0.1" @@ -77,6 +120,20 @@ dependencies = [ "thiserror", ] +[[package]] +name = "astroport-native-coin-registry" +version = "1.0.1" +source = "git+https://github.com/astroport-fi/astroport-core.git#80595f1e7a4967e3d606efef2111d0ac67747f9f" +dependencies = [ + "astroport 3.8.0", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-storage-plus 0.15.1", + "cw2 0.15.1", + "thiserror", +] + [[package]] name = "astroport-pair-stable" version = "2.1.4" @@ -93,6 +150,23 @@ dependencies = [ "thiserror", ] +[[package]] +name = "astroport-pair-stable" +version = "3.4.0" +source = "git+https://github.com/astroport-fi/astroport-core.git#80595f1e7a4967e3d606efef2111d0ac67747f9f" +dependencies = [ + "astroport 3.8.0", + "astroport-circular-buffer", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.15.1", + "cw-utils 1.0.3", + "cw2 0.15.1", + "cw20 0.15.1", + "itertools 0.10.5", + "thiserror", +] + [[package]] name = "astroport-token" version = "1.1.1" @@ -107,6 +181,20 @@ dependencies = [ "snafu", ] +[[package]] +name = "astroport-token" +version = "1.1.1" +source = "git+https://github.com/astroport-fi/astroport-core.git#80595f1e7a4967e3d606efef2111d0ac67747f9f" +dependencies = [ + "astroport 3.8.0", + "cosmwasm-schema", + "cosmwasm-std", + "cw2 0.15.1", + "cw20 0.15.1", + "cw20-base", + "snafu", +] + [[package]] name = "astroport-whitelist" version = "1.0.1" @@ -120,6 +208,19 @@ dependencies = [ "thiserror", ] +[[package]] +name = "astroport-whitelist" +version = "1.0.1" +source = "git+https://github.com/astroport-fi/astroport-core.git#80595f1e7a4967e3d606efef2111d0ac67747f9f" +dependencies = [ + "astroport 3.8.0", + "cosmwasm-schema", + "cosmwasm-std", + "cw1-whitelist 0.15.1", + "cw2 0.15.1", + "thiserror", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -140,9 +241,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.5" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64ct" @@ -176,9 +277,9 @@ dependencies = [ [[package]] name = "bnum" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128a44527fc0d6abf05f9eda748b9027536e12dff93f5acc8449f51583309350" +checksum = "ab9008b6bb9fc80b5277f2fe481c09e828743d9151203e804583eb4c9e15b31d" [[package]] name = "byteorder" @@ -212,9 +313,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "cosmos-sdk-proto" @@ -240,9 +341,9 @@ dependencies = [ [[package]] name = "cosmwasm-crypto" -version = "1.5.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8bb3c77c3b7ce472056968c745eb501c440fbc07be5004eba02782c35bfbbe3" +checksum = "8ed6aa9f904de106fa16443ad14ec2abe75e94ba003bb61c681c0e43d4c58d2a" dependencies = [ "digest 0.10.7", "ecdsa", @@ -254,18 +355,18 @@ dependencies = [ [[package]] name = "cosmwasm-derive" -version = "1.5.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea73e9162e6efde00018d55ed0061e93a108b5d6ec4548b4f8ce3c706249687" +checksum = "40abec852f3d4abec6d44ead9a58b78325021a1ead1e7229c3471414e57b2e49" dependencies = [ "syn 1.0.109", ] [[package]] name = "cosmwasm-schema" -version = "1.5.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0df41ea55f2946b6b43579659eec048cc2f66e8c8e2e3652fc5e5e476f673856" +checksum = "b166215fbfe93dc5575bae062aa57ae7bb41121cffe53bac33b033257949d2a9" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -276,9 +377,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.5.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43609e92ce1b9368aa951b334dd354a2d0dd4d484931a5f83ae10e12a26c8ba9" +checksum = "8bf12f8e20bb29d1db66b7ca590bc2f670b548d21e9be92499bc0f9022a994a8" dependencies = [ "proc-macro2", "quote", @@ -287,11 +388,11 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.5.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04d6864742e3a7662d024b51a94ea81c9af21db6faea2f9a6d2232bb97c6e53e" +checksum = "ad011ae7447188e26e4a7dbca2fcd0fc186aa21ae5c86df0503ea44c78f9e469" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", "bech32", "bnum", "cosmwasm-crypto", @@ -309,9 +410,9 @@ dependencies = [ [[package]] name = "cosmwasm-storage" -version = "1.5.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd2b4ae72a03e8f56c85df59d172d51d2d7dc9cec6e2bc811e3fb60c588032a4" +checksum = "66de2ab9db04757bcedef2b5984fbe536903ada4a8a9766717a4a71197ef34f6" dependencies = [ "cosmwasm-std", "serde", @@ -322,17 +423,18 @@ name = "covenant-astroport-liquid-pooler" version = "1.0.0" dependencies = [ "astroport 2.9.5 (registry+https://github.com/rust-lang/crates.io-index)", - "astroport-factory", - "astroport-native-coin-registry", - "astroport-pair-stable", - "astroport-token", - "astroport-whitelist", + "astroport-factory 1.5.1", + "astroport-native-coin-registry 1.0.1 (git+https://github.com/astroport-fi/astroport-core.git?rev=700f66d)", + "astroport-pair-stable 2.1.4", + "astroport-token 1.1.1 (git+https://github.com/astroport-fi/astroport-core.git?rev=700f66d)", + "astroport-whitelist 1.0.1 (git+https://github.com/astroport-fi/astroport-core.git?rev=700f66d)", "bech32", "cosmwasm-schema", "cosmwasm-std", "covenant-clock", "covenant-macros", "covenant-two-party-pol-holder", + "covenant-utils", "cw-multi-test", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", @@ -392,6 +494,7 @@ dependencies = [ "cw-storage-plus 1.2.0", "cw2 1.1.2", "neutron-sdk", + "osmosis-std 0.21.0", "prost 0.11.9", "prost-types 0.11.9", "protobuf 3.3.0", @@ -520,7 +623,7 @@ dependencies = [ "cw2 1.1.2", "cw20 1.1.2", "neutron-sdk", - "osmosis-std", + "osmosis-std 0.13.2", "polytone", "prost 0.11.9", "protobuf 3.3.0", @@ -543,7 +646,7 @@ dependencies = [ "cw1-whitelist 1.1.2", "cw2 1.1.2", "cw20 1.1.2", - "osmosis-std", + "osmosis-std 0.13.2", "polytone", "prost 0.11.9", "protobuf 3.3.0", @@ -552,6 +655,83 @@ dependencies = [ "thiserror", ] +[[package]] +name = "covenant-single-party-pol" +version = "1.0.0" +dependencies = [ + "anyhow", + "astroport 2.9.5 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.13.1", + "bech32", + "cosmos-sdk-proto 0.14.0", + "cosmwasm-schema", + "cosmwasm-std", + "covenant-astroport-liquid-pooler", + "covenant-clock", + "covenant-ibc-forwarder", + "covenant-interchain-router", + "covenant-interchain-splitter", + "covenant-native-splitter", + "covenant-single-party-pol-holder", + "covenant-stride-liquid-staker", + "covenant-utils", + "cw-multi-test", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "neutron-sdk", + "prost 0.11.9", + "prost-types 0.11.9", + "protobuf 3.3.0", + "schemars", + "serde", + "serde-json-wasm 0.4.1", + "sha2 0.10.8", + "thiserror", +] + +[[package]] +name = "covenant-single-party-pol-holder" +version = "1.0.0" +dependencies = [ + "anyhow", + "astroport-factory 1.6.0", + "astroport-native-coin-registry 1.0.1 (git+https://github.com/astroport-fi/astroport-core.git)", + "astroport-pair-stable 3.4.0", + "astroport-token 1.1.1 (git+https://github.com/astroport-fi/astroport-core.git)", + "astroport-whitelist 1.0.1 (git+https://github.com/astroport-fi/astroport-core.git)", + "cosmwasm-schema", + "cosmwasm-std", + "covenant-macros", + "covenant-utils", + "cw-multi-test", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "serde", + "thiserror", +] + +[[package]] +name = "covenant-stride-liquid-staker" +version = "1.0.0" +dependencies = [ + "cosmos-sdk-proto 0.14.0", + "cosmwasm-schema", + "cosmwasm-std", + "covenant-clock", + "covenant-macros", + "covenant-utils", + "cw-storage-plus 1.2.0", + "cw2 1.1.2", + "neutron-sdk", + "protobuf 3.3.0", + "schemars", + "serde", + "serde-json-wasm 0.4.1", + "thiserror", +] + [[package]] name = "covenant-swap" version = "1.0.0" @@ -663,17 +843,20 @@ dependencies = [ "cosmos-sdk-proto 0.14.0", "cosmwasm-schema", "cosmwasm-std", + "covenant-macros", + "cw-utils 1.0.3", "cw20 1.1.2", "neutron-sdk", "polytone", "prost 0.11.9", + "sha2 0.10.8", ] [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -686,9 +869,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-bigint" -version = "0.5.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -930,6 +1113,21 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cw3" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2967fbd073d4b626dd9e7148e05a84a3bebd9794e71342e12351110ffbb12395" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils 1.0.3", + "cw20 1.1.2", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "der" version = "0.7.8" @@ -986,9 +1184,9 @@ checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" [[package]] name = "ecdsa" -version = "0.16.8" +version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", "digest 0.10.7", @@ -1021,9 +1219,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "elliptic-curve" -version = "0.13.6" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97ca172ae9dc9f9b779a6e3a65d308f2af74e5b8c921299075bdb4a0370e914" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", @@ -1076,9 +1274,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", @@ -1149,15 +1347,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "k256" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" dependencies = [ "cfg-if", "ecdsa", @@ -1169,9 +1367,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.150" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "neutron-sdk" @@ -1222,9 +1420,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" @@ -1240,7 +1438,7 @@ checksum = "10d6fe6ac7fcba45ed61d738091d33c838c4cabbcf4892dc7aa56d19d39cc976" dependencies = [ "chrono", "cosmwasm-std", - "osmosis-std-derive", + "osmosis-std-derive 0.13.2", "prost 0.11.9", "prost-types 0.11.9", "schemars", @@ -1248,6 +1446,22 @@ dependencies = [ "serde-cw-value", ] +[[package]] +name = "osmosis-std" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e87adf61f03306474ce79ab322d52dfff6b0bcf3aed1e12d8864ac0400dec1bf" +dependencies = [ + "chrono", + "cosmwasm-std", + "osmosis-std-derive 0.20.1", + "prost 0.12.3", + "prost-types 0.12.3", + "schemars", + "serde", + "serde-cw-value", +] + [[package]] name = "osmosis-std-derive" version = "0.13.2" @@ -1260,6 +1474,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "osmosis-std-derive" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5ebdfd1bc8ed04db596e110c6baa9b174b04f6ed1ec22c666ddc5cb3fa91bd7" +dependencies = [ + "itertools 0.10.5", + "proc-macro2", + "prost-types 0.11.9", + "quote", + "syn 1.0.109", +] + [[package]] name = "paste" version = "1.0.14" @@ -1290,9 +1517,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] @@ -1340,7 +1567,7 @@ dependencies = [ "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -1393,9 +1620,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -1427,9 +1654,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "schemars" @@ -1471,15 +1698,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" dependencies = [ "serde_derive", ] @@ -1522,22 +1749,22 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.12" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff" +checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -1553,9 +1780,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ "itoa", "ryu", @@ -1564,13 +1791,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145" +checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -1599,9 +1826,9 @@ dependencies = [ [[package]] name = "signature" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", "rand_core 0.6.4", @@ -1630,9 +1857,9 @@ dependencies = [ [[package]] name = "spki" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", @@ -1672,9 +1899,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -1719,22 +1946,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -1792,6 +2019,6 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "zeroize" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/Cargo.toml b/Cargo.toml index 4373913b..c8156bc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,9 @@ members = [ resolver = "2" [workspace.package] -edition = "2021" -license = "BSD-3" -version = "1.0.0" +edition = "2021" +license = "BSD-3" +version = "1.0.0" repository = "https://github.com/timewave-computer/covenants" # rust-version = "1.71.0" @@ -24,61 +24,63 @@ panic = 'abort' rpath = false [workspace.dependencies] -covenant-clock = { path = "contracts/clock" } -covenant-clock-tester = { path = "contracts/clock-tester" } -covenant-ibc-forwarder = { path = "contracts/ibc-forwarder" } -covenant-native-splitter = { path = "contracts/native-splitter" } -covenant-interchain-splitter = { path = "contracts/interchain-splitter" } -covenant-swap-holder = { path = "contracts/swap-holder" } -swap-covenant = { path = "contracts/swap-covenant" } -covenant-interchain-router = { path = "contracts/interchain-router" } -covenant-two-party-pol-holder = { path = "contracts/two-party-pol-holder" } -covenant-two-party-pol = { path = "contracts/two-party-pol-covenant" } +covenant-clock = { path = "contracts/clock" } +covenant-clock-tester = { path = "contracts/clock-tester" } +covenant-ibc-forwarder = { path = "contracts/ibc-forwarder" } +covenant-native-splitter = { path = "contracts/native-splitter" } +covenant-interchain-splitter = { path = "contracts/interchain-splitter" } +covenant-swap-holder = { path = "contracts/swap-holder" } +swap-covenant = { path = "contracts/swap-covenant" } +covenant-interchain-router = { path = "contracts/interchain-router" } +covenant-two-party-pol-holder = { path = "contracts/two-party-pol-holder" } +covenant-two-party-pol = { path = "contracts/two-party-pol-covenant" } covenant-astroport-liquid-pooler = { path = "contracts/astroport-liquid-pooler" } covenant-native-router = { path = "contracts/native-router" } covenant-osmo-liquid-pooler = { path = "contracts/osmo-liquid-pooler" } covenant-outpost-osmo-liquid-pooler = { path = "contracts/outpost-osmo-liquid-pooler" } +covenant-single-party-pol-holder = { path = "contracts/single-party-pol-holder"} +covenant-stride-liquid-staker = { path = "contracts/stride-liquid-staker" } # packages polytone = "1.0.0" clock-derive = { path = "packages/clock-derive" } cw-fifo = { path = "packages/cw-fifo" } covenant-macros = { path = "packages/covenant-macros" } -covenant-utils = { path = "packages/covenant-utils" } +covenant-utils = { path = "packages/covenant-utils" } # the sha2 version here is the same as the one used by # cosmwasm-std. when bumping cosmwasm-std, this should also be # updated. to find cosmwasm_std's sha function: # ```cargo tree --package cosmwasm-std``` -sha2 = "0.10.8" -neutron-sdk = { git = "https://github.com/neutron-org/neutron-sdk", branch = "feat/cw-dex-bindings" } +sha2 = "0.10.8" +neutron-sdk = { git = "https://github.com/neutron-org/neutron-sdk", branch = "feat/cw-dex-bindings" } cosmos-sdk-proto = { version = "0.14.0", default-features = false } -protobuf = { version = "3.2.0", features = ["with-bytes"] } -serde-json-wasm = { version = "0.4.1" } -base64 = "0.13.0" -prost = "0.11" -astroport = "2.9.5" -prost-types = "0.11" -bech32 = "0.9.0" -cosmwasm-schema = "1.5.0" -cosmwasm-std = { version = "1.5.0", features = ["ibc3", "cosmwasm_1_1", "cosmwasm_1_2"] } -cw-storage-plus = "1.2.0" -cw-utils = "1.0.3" -getrandom = { version = "0.2", features = ["js"] } -cw2 = "1.0.1" -serde = { version = "1.0.145", default-features = false, features = ["derive"] } -thiserror = "1.0.31" -schemars = "0.8.10" +protobuf = { version = "3.2.0", features = ["with-bytes"] } +serde-json-wasm = { version = "0.4.1" } +base64 = "0.13.0" +prost = "0.11" +astroport = "2.9.5" +prost-types = "0.11" +bech32 = "0.9.0" +cosmwasm-schema = "1.5.0" +cosmwasm-std = { version = "1.5.0", features = ["ibc3", "cosmwasm_1_1", "cosmwasm_1_2"] } +cw-storage-plus = "1.2.0" +cw-utils = "1.0.3" +getrandom = { version = "0.2", features = ["js"] } +cw2 = "1.0.1" +serde = { version = "1.0.145", default-features = false, features = ["derive"] } +thiserror = "1.0.31" +schemars = "0.8.10" cw20 = { version = "1.1.2" } -proc-macro2 = "1" -quote = "1" -syn = "1" +proc-macro2 = "1" +quote = "1" +syn = "1" # dev-dependencies -cw-multi-test = "0.20.0" -anyhow = { version = "1.0.51" } -cw1-whitelist = "1.1.2" -astroport-token = {git = "https://github.com/astroport-fi/astroport-core.git", rev="700f66d"} -astroport-whitelist = {git = "https://github.com/astroport-fi/astroport-core.git", rev="700f66d"} -astroport-factory = {git = "https://github.com/astroport-fi/astroport-core.git", rev="700f66d"} -astroport-native-coin-registry = {git = "https://github.com/astroport-fi/astroport-core.git", rev="700f66d"} -astroport-pair-stable = {git = "https://github.com/astroport-fi/astroport-core.git", rev="700f66d"} +cw-multi-test = "0.20.0" +anyhow = { version = "1.0.51" } +cw1-whitelist = "1.1.2" +astroport-token = { git = "https://github.com/astroport-fi/astroport-core.git", rev = "700f66d" } +astroport-whitelist = { git = "https://github.com/astroport-fi/astroport-core.git", rev = "700f66d" } +astroport-factory = { git = "https://github.com/astroport-fi/astroport-core.git", rev = "700f66d" } +astroport-native-coin-registry = { git = "https://github.com/astroport-fi/astroport-core.git", rev = "700f66d" } +astroport-pair-stable = { git = "https://github.com/astroport-fi/astroport-core.git", rev = "700f66d" } diff --git a/contracts/astroport-liquid-pooler/Cargo.toml b/contracts/astroport-liquid-pooler/Cargo.toml index 8af731ea..79a2a214 100644 --- a/contracts/astroport-liquid-pooler/Cargo.toml +++ b/contracts/astroport-liquid-pooler/Cargo.toml @@ -13,7 +13,6 @@ exclude = [ "hash.txt", ] - [lib] crate-type = ["cdylib", "rlib"] @@ -25,7 +24,7 @@ library = [] [dependencies] covenant-macros = { workspace = true } -covenant-clock = { workspace = true, features=["library"] } +covenant-clock = { workspace = true, features = ["library"] } cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } @@ -38,22 +37,23 @@ thiserror = { workspace = true } # cosmwasm-std. when bumping cosmwasm-std, this should also be # updated. to find cosmwasm_std's sha function: # ```cargo tree --package cosmwasm-std``` -sha2 = { workspace = true } -neutron-sdk = { workspace = true } -protobuf = { workspace = true } -schemars = { workspace = true } -bech32 = { workspace = true } -astroport = { workspace = true } -cw20 = { workspace = true } +sha2 = { workspace = true } +neutron-sdk = { workspace = true } +protobuf = { workspace = true } +schemars = { workspace = true } +bech32 = { workspace = true } +astroport = { workspace = true } +cw20 = { workspace = true } +covenant-utils = { workspace = true } # dev-dependencies [dev-dependencies] -cw-multi-test = { workspace = true } -astroport = { workspace = true } -cw1-whitelist = { workspace = true } -covenant-two-party-pol-holder = { workspace = true } -astroport-token = { workspace = true } -astroport-whitelist = { workspace = true } -astroport-factory = { workspace = true } +cw-multi-test = { workspace = true } +astroport = { workspace = true } +cw1-whitelist = { workspace = true } +covenant-two-party-pol-holder = { workspace = true } +astroport-token = { workspace = true } +astroport-whitelist = { workspace = true } +astroport-factory = { workspace = true } astroport-native-coin-registry = { workspace = true } -astroport-pair-stable = { workspace = true } +astroport-pair-stable = { workspace = true } diff --git a/contracts/astroport-liquid-pooler/src/contract.rs b/contracts/astroport-liquid-pooler/src/contract.rs index 50631c96..9c7d0731 100644 --- a/contracts/astroport-liquid-pooler/src/contract.rs +++ b/contracts/astroport-liquid-pooler/src/contract.rs @@ -1,18 +1,20 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_json_binary, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, + ensure, to_json_binary, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, QuerierWrapper, Reply, Response, StdError, StdResult, SubMsg, Uint128, WasmMsg, }; use covenant_clock::helpers::{enqueue_msg, verify_clock}; +use covenant_utils::{query_astro_pool_token, withdraw_lp_helper::WithdrawLPMsgs}; use cw2::set_contract_version; use astroport::{ asset::{Asset, PairInfo}, factory::PairType, - pair::{ExecuteMsg::ProvideLiquidity, PoolResponse}, + pair::{Cw20HookMsg, ExecuteMsg::ProvideLiquidity, PoolResponse}, DecimalCheckedOps, }; +use cw20::Cw20ExecuteMsg; use cw_utils::parse_reply_instantiate_data; use crate::{ @@ -94,9 +96,80 @@ pub fn execute( ) -> Result { match msg { ExecuteMsg::Tick {} => try_tick(deps, env, info), + ExecuteMsg::Withdraw { percentage } => try_withdraw(deps, env, info, percentage), } } +fn try_withdraw( + deps: DepsMut, + env: Env, + info: MessageInfo, + percent: Option, +) -> Result { + let percent = percent.unwrap_or(Decimal::one()); + // Verify percentage is < 1 and > 0 + let holder_addr = HOLDER_ADDRESS + .load(deps.storage) + .map_err(|_| ContractError::MissingHolderError {})?; + + ensure!(info.sender == holder_addr, ContractError::NotHolder {}); + + // Query LP position of the LPer + let lp_config = LP_CONFIG.load(deps.storage)?; + let lp_token_info = query_astro_pool_token( + deps.querier, + lp_config.pool_address.to_string(), + env.contract.address.to_string(), + )?; + + // If percentage is 100%, use the whole balance + // If percentage is less than 100%, calculate the percentage of share we want to withdraw + let withdraw_shares_amount = if percent == Decimal::one() { + lp_token_info.balance_response.balance + } else { + Decimal::from_atomics(lp_token_info.balance_response.balance, 0)? + .checked_mul(percent)? + .to_uint_floor() + }; + + // Clculate the withdrawn amount of A and B tokens from the shares we have + let withdrawn_coins = deps + .querier + .query_wasm_smart::>( + lp_config.pool_address.to_string(), + &astroport::pair::QueryMsg::Share { + amount: withdraw_shares_amount, + }, + )? + .iter() + .map(|asset| asset.to_coin()) + .collect::, _>>()?; + + // exit pool and withdraw funds with the shares calculated + let withdraw_liquidity_hook = &Cw20HookMsg::WithdrawLiquidity { assets: vec![] }; + let withdraw_msg = WasmMsg::Execute { + contract_addr: lp_token_info.pair_info.liquidity_token.to_string(), + msg: to_json_binary(&Cw20ExecuteMsg::Send { + contract: lp_config.pool_address.to_string(), + amount: withdraw_shares_amount, + msg: to_json_binary(withdraw_liquidity_hook)?, + })?, + funds: vec![], + }; + + // send message to holder that we finished with the withdrawal + // with the funds we withdrew from the pool + let to_holder_msg = WasmMsg::Execute { + contract_addr: holder_addr.to_string(), + msg: to_json_binary(&WithdrawLPMsgs::Distribute {})?, + funds: withdrawn_coins, + }; + + Ok(Response::default() + .add_message(withdraw_msg) + .add_message(to_holder_msg)) +} + /// attempts to advance the state machine. performs `info.sender` validation. fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> Result { // Verify caller is the clock @@ -165,7 +238,7 @@ fn try_lp(mut deps: DepsMut, env: Env) -> Result { // exactly one balance is non-zero, we attempt single-side (true, false) | (false, true) => { let single_sided_submsg = - try_get_single_side_lp_submsg(deps.branch(), coin_a, coin_b, lp_config)?; + try_get_single_side_lp_submsg(deps.branch(), env, (coin_a, coin_b), lp_config)?; if let Some(msg) = single_sided_submsg { return Ok(Response::default() .add_submessage(msg) @@ -176,11 +249,10 @@ fn try_lp(mut deps: DepsMut, env: Env) -> Result { (false, false) => { let double_sided_submsg = try_get_double_side_lp_submsg( deps.branch(), - coin_a, - coin_b, + env, + (coin_a, coin_b), a_to_b_ratio, - pool_token_a_bal, - pool_token_b_bal, + (pool_token_a_bal, pool_token_b_bal), lp_config, )?; if let Some(msg) = double_sided_submsg { @@ -205,18 +277,12 @@ fn try_lp(mut deps: DepsMut, env: Env) -> Result { /// the existing pool ratio. fn try_get_double_side_lp_submsg( deps: DepsMut, - token_a: Coin, - token_b: Coin, + env: Env, + (token_a, token_b): (Coin, Coin), pool_token_ratio: Decimal, - pool_token_a_bal: Uint128, - pool_token_b_bal: Uint128, + (pool_token_a_bal, pool_token_b_bal): (Uint128, Uint128), lp_config: LpConfig, ) -> Result, ContractError> { - let holder_address = match HOLDER_ADDRESS.may_load(deps.storage)? { - Some(addr) => addr, - None => return Err(ContractError::MissingHolderError {}), - }; - // we thus find the required token amount to enter into the position using all available b tokens: let required_token_a_amount = pool_token_ratio.checked_mul_uint128(token_b.amount)?; @@ -248,7 +314,7 @@ fn try_get_double_side_lp_submsg( assets: vec![asset_a_double_sided, asset_b_double_sided], slippage_tolerance: lp_config.slippage_tolerance, auto_stake: Some(false), - receiver: Some(holder_address.to_string()), + receiver: Some(env.contract.address.to_string()), }; // update the provided amounts and leftover assets @@ -277,15 +343,10 @@ fn try_get_double_side_lp_submsg( /// single-side liquidity limits, we provide it. fn try_get_single_side_lp_submsg( deps: DepsMut, - coin_a: Coin, - coin_b: Coin, + env: Env, + (coin_a, coin_b): (Coin, Coin), lp_config: LpConfig, ) -> Result, ContractError> { - let holder_address = match HOLDER_ADDRESS.may_load(deps.storage)? { - Some(addr) => addr, - None => return Err(ContractError::MissingHolderError {}), - }; - let assets = lp_config .asset_data .to_asset_vec(coin_a.amount, coin_b.amount); @@ -295,7 +356,7 @@ fn try_get_single_side_lp_submsg( assets, slippage_tolerance: lp_config.slippage_tolerance, auto_stake: Some(false), - receiver: Some(holder_address.to_string()), + receiver: Some(env.contract.address.to_string()), }; // now we try to submit the message for either B or A token single side liquidity @@ -439,8 +500,7 @@ fn handle_double_sided_reply_id( _env: Env, msg: Reply, ) -> Result { - let parsed_data = parse_reply_instantiate_data(msg); - match parsed_data { + match parse_reply_instantiate_data(msg) { Ok(response) => Ok(Response::default() .add_attribute("method", "handle_double_sided_reply_id") .add_attribute( diff --git a/contracts/astroport-liquid-pooler/src/error.rs b/contracts/astroport-liquid-pooler/src/error.rs index e053f83c..90fc299c 100644 --- a/contracts/astroport-liquid-pooler/src/error.rs +++ b/contracts/astroport-liquid-pooler/src/error.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{OverflowError, StdError}; +use cosmwasm_std::{DecimalRangeExceeded, OverflowError, StdError}; use neutron_sdk::NeutronError; use thiserror::Error; @@ -7,12 +7,15 @@ pub enum ContractError { #[error("{0}")] Std(#[from] StdError), - #[error("{0}")] + #[error(transparent)] NeutronError(#[from] NeutronError), - #[error("{0}")] + #[error(transparent)] OverflowError(#[from] OverflowError), + #[error(transparent)] + DecimalRangeExceeded(#[from] DecimalRangeExceeded), + #[error("Not clock")] ClockVerificationError {}, @@ -42,4 +45,7 @@ pub enum ContractError { #[error("Pair type mismatch")] PairTypeMismatch {}, + + #[error("Only holder can withdraw the position")] + NotHolder {}, } diff --git a/contracts/astroport-liquid-pooler/src/msg.rs b/contracts/astroport-liquid-pooler/src/msg.rs index 6a39c2bf..216e30f2 100644 --- a/contracts/astroport-liquid-pooler/src/msg.rs +++ b/contracts/astroport-liquid-pooler/src/msg.rs @@ -4,7 +4,9 @@ use astroport::{ }; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{to_json_binary, Addr, Attribute, Binary, Decimal, StdError, Uint128, WasmMsg}; -use covenant_macros::{clocked, covenant_clock_address, covenant_deposit_address}; +use covenant_macros::{ + clocked, covenant_clock_address, covenant_deposit_address, covenant_lper_withdraw, +}; use crate::error::ContractError; @@ -196,6 +198,7 @@ pub struct SingleSideLpLimits { } #[clocked] +#[covenant_lper_withdraw] #[cw_serde] pub enum ExecuteMsg {} diff --git a/contracts/ibc-forwarder/Cargo.toml b/contracts/ibc-forwarder/Cargo.toml index 301e145e..51d9430b 100644 --- a/contracts/ibc-forwarder/Cargo.toml +++ b/contracts/ibc-forwarder/Cargo.toml @@ -41,6 +41,7 @@ prost = { workspace = true } prost-types = { workspace = true } bech32 = { workspace = true } covenant-utils = { workspace = true } +osmosis-std = "0.21.0" [dev-dependencies] cw-multi-test = { workspace = true } diff --git a/contracts/ibc-forwarder/src/contract.rs b/contracts/ibc-forwarder/src/contract.rs index acabfb52..9ff73be4 100644 --- a/contracts/ibc-forwarder/src/contract.rs +++ b/contracts/ibc-forwarder/src/contract.rs @@ -1,12 +1,14 @@ -use cosmos_sdk_proto::ibc::applications::transfer::v1::MsgTransfer; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - coin, from_json, to_json_binary, to_json_vec, Binary, Coin, CosmosMsg, CustomQuery, Deps, - DepsMut, Env, MessageInfo, Reply, Response, StdError, StdResult, Storage, SubMsg, + from_json, to_json_binary, to_json_vec, Binary, Coin, CosmosMsg, CustomQuery, Deps, DepsMut, + Env, MessageInfo, Reply, Response, StdError, StdResult, Storage, SubMsg, }; use covenant_clock::helpers::{enqueue_msg, verify_clock}; -use covenant_utils::neutron_ica::{self, get_proto_coin, RemoteChainInfo}; +use covenant_utils::{ + get_default_ica_fee, + neutron_ica::{self, get_proto_coin, RemoteChainInfo}, +}; use cw2::set_contract_version; use neutron_sdk::{ bindings::{ @@ -14,16 +16,18 @@ use neutron_sdk::{ query::NeutronQuery, }, interchain_txs::helpers::get_port_id, - sudo::msg::{RequestPacket, SudoMsg}, + sudo::msg::SudoMsg, NeutronError, NeutronResult, }; use crate::{ + helpers::{get_next_memo, MsgTransfer}, msg::{ContractState, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, state::{ CLOCK_ADDRESS, CONTRACT_STATE, INTERCHAIN_ACCOUNTS, NEXT_CONTRACT, REMOTE_CHAIN_INFO, REPLY_ID_STORAGE, SUDO_PAYLOAD, TRANSFER_AMOUNT, }, + sudo::{save_reply_payload, sudo_error, sudo_open_ack, sudo_response, sudo_timeout}, }; const CONTRACT_NAME: &str = "crates.io:covenant-ibc-forwarder"; @@ -97,7 +101,7 @@ fn try_tick(deps: ExecuteDeps, env: Env, info: MessageInfo) -> NeutronResult NeutronResult> { let remote_chain_info = REMOTE_CHAIN_INFO.load(deps.storage)?; - let register_fee: Option> = Some(vec![coin(1000001, "untrn")]); + let register_fee: Option> = Some(vec![get_default_ica_fee()]); let register_msg = NeutronMsg::register_interchain_account( remote_chain_info.connection_id, INTERCHAIN_ACCOUNT_ID.to_string(), @@ -118,7 +122,7 @@ fn try_forward_funds(env: Env, mut deps: ExecuteDeps) -> NeutronResult = deps.querier.query_wasm_smart( - next_contract, + next_contract.to_string(), &covenant_utils::neutron_ica::QueryMsg::DepositAddress {}, )?; @@ -137,6 +141,8 @@ fn try_forward_funds(env: Env, mut deps: ExecuteDeps) -> NeutronResult NeutronResult StdResult { } } -// handler -fn sudo_open_ack( - deps: ExecuteDeps, - _env: Env, - port_id: String, - _channel_id: String, - _counterparty_channel_id: String, - counterparty_version: String, -) -> StdResult { - // The version variable contains a JSON value with multiple fields, - // including the generated account address. - let parsed_version: Result = - serde_json_wasm::from_str(counterparty_version.as_str()); - - // get the parsed OpenAckVersion or return an error if we fail - let Ok(parsed_version) = parsed_version else { - return Err(StdError::generic_err("Can't parse counterparty_version")); - }; - - // Update the storage record associated with the interchain account. - INTERCHAIN_ACCOUNTS.save( - deps.storage, - port_id, - &Some(( - parsed_version.clone().address, - parsed_version.controller_connection_id, - )), - )?; - CONTRACT_STATE.save(deps.storage, &ContractState::IcaCreated)?; - - Ok(Response::default().add_attribute("method", "sudo_open_ack")) -} - -fn sudo_response(deps: ExecuteDeps, request: RequestPacket, data: Binary) -> StdResult { - deps.api - .debug(format!("WASMDEBUG: sudo_response: sudo received: {request:?} {data:?}").as_str()); - - // either of these errors will close the channel - request - .sequence - .ok_or_else(|| StdError::generic_err("sequence not found"))?; - - request - .source_channel - .ok_or_else(|| StdError::generic_err("channel_id not found"))?; - - Ok(Response::default().add_attribute("method", "sudo_response")) -} - -fn sudo_timeout(deps: ExecuteDeps, _env: Env, request: RequestPacket) -> StdResult { - deps.api - .debug(format!("WASMDEBUG: sudo timeout request: {request:?}").as_str()); - - // revert the state to Instantiated to force re-creation of ICA - CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; - - // returning Ok as this is anticipated. channel is already closed. - Ok(Response::default()) -} - -fn sudo_error(deps: ExecuteDeps, request: RequestPacket, details: String) -> StdResult { - deps.api - .debug(format!("WASMDEBUG: sudo error: {details}").as_str()); - - deps.api - .debug(format!("WASMDEBUG: request packet: {request:?}").as_str()); - - // either of these errors will close the channel - request - .sequence - .ok_or_else(|| StdError::generic_err("sequence not found"))?; - - request - .source_channel - .ok_or_else(|| StdError::generic_err("channel_id not found"))?; - - Ok(Response::default().add_attribute("method", "sudo_error")) -} - -pub fn save_reply_payload( - store: &mut dyn Storage, - payload: neutron_ica::SudoPayload, -) -> StdResult<()> { - REPLY_ID_STORAGE.save(store, &to_json_vec(&payload)?) -} - #[cfg_attr(not(feature = "library"), entry_point)] pub fn reply(deps: ExecuteDeps, env: Env, msg: Reply) -> StdResult { deps.api diff --git a/contracts/ibc-forwarder/src/helpers.rs b/contracts/ibc-forwarder/src/helpers.rs new file mode 100644 index 00000000..82c3c8f6 --- /dev/null +++ b/contracts/ibc-forwarder/src/helpers.rs @@ -0,0 +1,83 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{QuerierWrapper, StdResult}; +use neutron_sdk::bindings::query::NeutronQuery; + +/// Query next contract for the memo field +/// If query failed, we set memo to empty string, meaning no memo is expected +/// If query returns an empty string, we error out because we expect the memo not to be empty +/// +/// We do that because not all next contract will need a memo, and if the next contract +/// doesn't have the NextMemo query, we don't want to error out, rather return an empty memo. +/// Thats why if the NextMemo query doesn't fail, we expect it to return a non-empty string. +/// +/// This requires the next contract to implement the NextMemo query if it needs it, and +/// be careful not to return an error. +pub(crate) fn get_next_memo( + querier: QuerierWrapper, + addr: &str, +) -> StdResult { + #[cw_serde] + enum Query { + NextMemo {}, + } + + // We check that the query was successful, if not, we return empty string + let Ok(memo) = querier.query_wasm_smart::(addr.to_string(), &Query::NextMemo {}) else { + return Ok("".to_string()); + }; + + // If the query was successful, we expect the memo to be non-empty + // If memo is empty, something went wrong in the query, so we should error and retry later + if memo.is_empty() { + Err(cosmwasm_std::StdError::generic_err( + "NextMemo query returned empty string", + )) + } else { + Ok(memo) + } +} + +#[derive( + Clone, + PartialEq, + Eq, + ::prost::Message, + serde::Serialize, + serde::Deserialize, + schemars::JsonSchema, +)] +pub struct IbcCounterpartyHeight { + #[prost(uint64, optional, tag = "1")] + revision_number: Option, + #[prost(uint64, optional, tag = "2")] + revision_height: Option, +} + +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MsgTransfer { + /// the port on which the packet will be sent + #[prost(string, tag = "1")] + pub source_port: String, + /// the channel by which the packet will be sent + #[prost(string, tag = "2")] + pub source_channel: String, + /// the tokens to be transferred + #[prost(message, optional, tag = "3")] + pub token: Option, + /// the sender address + #[prost(string, tag = "4")] + pub sender: String, + /// the recipient address on the destination chain + #[prost(string, tag = "5")] + pub receiver: String, + /// Timeout height relative to the current block height. + /// The timeout is disabled when set to 0. + #[prost(message, optional, tag = "6")] + pub timeout_height: Option, + /// Timeout timestamp in absolute nanoseconds since unix epoch. + /// The timeout is disabled when set to 0. + #[prost(uint64, tag = "7")] + pub timeout_timestamp: u64, + #[prost(string, tag = "8")] + pub memo: String, +} diff --git a/contracts/ibc-forwarder/src/lib.rs b/contracts/ibc-forwarder/src/lib.rs index 0faea8f4..d6c588c4 100644 --- a/contracts/ibc-forwarder/src/lib.rs +++ b/contracts/ibc-forwarder/src/lib.rs @@ -4,5 +4,7 @@ extern crate core; pub mod contract; pub mod error; +pub mod helpers; pub mod msg; pub mod state; +pub mod sudo; diff --git a/contracts/ibc-forwarder/src/sudo.rs b/contracts/ibc-forwarder/src/sudo.rs new file mode 100644 index 00000000..29ba3afb --- /dev/null +++ b/contracts/ibc-forwarder/src/sudo.rs @@ -0,0 +1,104 @@ +use cosmwasm_std::{to_json_vec, Binary, DepsMut, Env, Response, StdError, StdResult, Storage}; +use covenant_utils::neutron_ica; +use neutron_sdk::{bindings::query::NeutronQuery, sudo::msg::RequestPacket}; + +use crate::{ + msg::ContractState, + state::{CONTRACT_STATE, INTERCHAIN_ACCOUNTS, REPLY_ID_STORAGE}, +}; + +type ExecuteDeps<'a> = DepsMut<'a, NeutronQuery>; + +// handler +pub fn sudo_open_ack( + deps: ExecuteDeps, + _env: Env, + port_id: String, + _channel_id: String, + _counterparty_channel_id: String, + counterparty_version: String, +) -> StdResult { + // The version variable contains a JSON value with multiple fields, + // including the generated account address. + let parsed_version: Result = + serde_json_wasm::from_str(counterparty_version.as_str()); + + // get the parsed OpenAckVersion or return an error if we fail + let Ok(parsed_version) = parsed_version else { + return Err(StdError::generic_err("Can't parse counterparty_version")); + }; + + // Update the storage record associated with the interchain account. + INTERCHAIN_ACCOUNTS.save( + deps.storage, + port_id, + &Some(( + parsed_version.clone().address, + parsed_version.controller_connection_id, + )), + )?; + CONTRACT_STATE.save(deps.storage, &ContractState::IcaCreated)?; + + Ok(Response::default().add_attribute("method", "sudo_open_ack")) +} + +pub fn sudo_response( + deps: ExecuteDeps, + request: RequestPacket, + data: Binary, +) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: sudo_response: sudo received: {request:?} {data:?}").as_str()); + + // either of these errors will close the channel + request + .sequence + .ok_or_else(|| StdError::generic_err("sequence not found"))?; + + request + .source_channel + .ok_or_else(|| StdError::generic_err("channel_id not found"))?; + + Ok(Response::default().add_attribute("method", "sudo_response")) +} + +pub fn sudo_timeout(deps: ExecuteDeps, _env: Env, request: RequestPacket) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: sudo timeout request: {request:?}").as_str()); + + // revert the state to Instantiated to force re-creation of ICA + CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; + + // returning Ok as this is anticipated. channel is already closed. + Ok(Response::default()) +} + +pub fn sudo_error( + deps: ExecuteDeps, + request: RequestPacket, + details: String, +) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: sudo error: {details}").as_str()); + + deps.api + .debug(format!("WASMDEBUG: request packet: {request:?}").as_str()); + + // either of these errors will close the channel + request + .sequence + .ok_or_else(|| StdError::generic_err("sequence not found"))?; + + request + .source_channel + .ok_or_else(|| StdError::generic_err("channel_id not found"))?; + + Ok(Response::default().add_attribute("method", "sudo_error")) +} + +pub fn save_reply_payload( + store: &mut dyn Storage, + payload: neutron_ica::SudoPayload, +) -> StdResult<()> { + REPLY_ID_STORAGE.save(store, &to_json_vec(&payload)?) +} diff --git a/contracts/interchain-router/src/suite_tests/tests.rs b/contracts/interchain-router/src/suite_tests/tests.rs index b5f7b2f0..30708486 100644 --- a/contracts/interchain-router/src/suite_tests/tests.rs +++ b/contracts/interchain-router/src/suite_tests/tests.rs @@ -134,7 +134,7 @@ fn test_tick() { .time .plus_seconds(Uint64::new(10).u64()) .nanos(), - memo: format!("ibc_distribution: denom1:{:?}", Uint128::new(100),).to_string(), + memo: format!("ibc_distribution: denom1:{:?}", Uint128::new(100)), fee: IbcFee { // must be empty recv_fee: vec![], diff --git a/contracts/native-splitter/Cargo.toml b/contracts/native-splitter/Cargo.toml index 1a455e90..0bd806b4 100644 --- a/contracts/native-splitter/Cargo.toml +++ b/contracts/native-splitter/Cargo.toml @@ -4,7 +4,7 @@ authors = ["benskey bekauz@protonmail.com"] description = "Native Splitter module for covenants" edition = { workspace = true } license = { workspace = true } -# rust-version = { workspace = true } +repository = { workspace = true } version = { workspace = true } [lib] @@ -17,9 +17,9 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -covenant-macros = { workspace = true } -covenant-clock = { workspace = true, features=["library"] } -covenant-utils = { workspace = true } +covenant-macros = { workspace = true } +covenant-clock = { workspace = true, features=["library"] } +covenant-utils = { workspace = true } cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw-storage-plus = { workspace = true } diff --git a/contracts/native-splitter/src/contract.rs b/contracts/native-splitter/src/contract.rs index bffd2cd8..4119671e 100644 --- a/contracts/native-splitter/src/contract.rs +++ b/contracts/native-splitter/src/contract.rs @@ -2,33 +2,36 @@ use std::collections::HashSet; use cosmos_sdk_proto::cosmos::bank::v1beta1::{Input, MsgMultiSend, Output}; use cosmos_sdk_proto::cosmos::base::v1beta1::Coin; +use cosmos_sdk_proto::traits::Message; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_json_binary, Attribute, Binary, CosmosMsg, CustomQuery, Deps, DepsMut, Env, MessageInfo, - Reply, Response, StdError, StdResult, SubMsg, Uint128, + to_json_binary, Attribute, Binary, CosmosMsg, CustomQuery, Deps, DepsMut, Env, Fraction, + MessageInfo, Reply, Response, StdError, StdResult, SubMsg, }; -use covenant_clock::helpers::verify_clock; -use covenant_utils::neutron_ica::{self, OpenAckVersion, RemoteChainInfo, SudoPayload}; +use covenant_clock::helpers::{enqueue_msg, verify_clock}; +use covenant_utils::get_default_ica_fee; +use covenant_utils::neutron_ica::{RemoteChainInfo, SudoPayload}; use cw2::set_contract_version; -use neutron_sdk::bindings::msg::MsgSubmitTxResponse; -use neutron_sdk::interchain_txs::helpers::{ - decode_acknowledgement_response, decode_message_response, get_port_id, -}; -use neutron_sdk::sudo::msg::{RequestPacket, SudoMsg}; +use neutron_sdk::bindings::types::ProtobufAny; +use neutron_sdk::interchain_txs::helpers::get_port_id; +use neutron_sdk::sudo::msg::SudoMsg; use neutron_sdk::NeutronError; use crate::msg::{ContractState, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; use crate::state::{ - add_error_to_queue, read_reply_payload, read_sudo_payload, save_reply_payload, - save_sudo_payload, CLOCK_ADDRESS, CONTRACT_STATE, INTERCHAIN_ACCOUNTS, REMOTE_CHAIN_INFO, + save_reply_payload, CLOCK_ADDRESS, CONTRACT_STATE, INTERCHAIN_ACCOUNTS, REMOTE_CHAIN_INFO, SPLIT_CONFIG_MAP, TRANSFER_AMOUNT, }; +use crate::sudo::{prepare_sudo_payload, sudo_error, sudo_open_ack, sudo_response, sudo_timeout}; use neutron_sdk::{ bindings::{msg::NeutronMsg, query::NeutronQuery}, NeutronResult, }; +type QueryDeps<'a> = Deps<'a, NeutronQuery>; +type ExecuteDeps<'a> = DepsMut<'a, NeutronQuery>; + const INTERCHAIN_ACCOUNT_ID: &str = "rc-ica"; const CONTRACT_NAME: &str = "crates.io:covenant-native-splitter"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -37,7 +40,7 @@ const SUDO_PAYLOAD_REPLY_ID: u64 = 1u64; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( - deps: DepsMut, + deps: ExecuteDeps, _env: Env, _info: MessageInfo, msg: InstantiateMsg, @@ -57,6 +60,7 @@ pub fn instantiate( }; REMOTE_CHAIN_INFO.save(deps.storage, &remote_chain_info)?; CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; + TRANSFER_AMOUNT.save(deps.storage, &msg.amount)?; // validate each split and store it in a map let mut split_resp_attributes: Vec = Vec::with_capacity(msg.splits.len()); @@ -80,6 +84,7 @@ pub fn instantiate( } Ok(Response::default() + .add_message(enqueue_msg(clock_addr.as_str())?) .add_attribute("method", "native_splitter_instantiate") .add_attribute("clock_address", clock_addr) .add_attributes(remote_chain_info.get_response_attributes()) @@ -88,7 +93,7 @@ pub fn instantiate( #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( - deps: DepsMut, + deps: ExecuteDeps, env: Env, info: MessageInfo, msg: ExecuteMsg, @@ -101,12 +106,11 @@ pub fn execute( } /// attempts to advance the state machine. performs `info.sender` validation -fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> NeutronResult> { +fn try_tick(deps: ExecuteDeps, env: Env, info: MessageInfo) -> NeutronResult> { // Verify caller is the clock verify_clock(&info.sender, &CLOCK_ADDRESS.load(deps.storage)?)?; - let current_state = CONTRACT_STATE.load(deps.storage)?; - match current_state { + match CONTRACT_STATE.load(deps.storage)? { ContractState::Instantiated => try_register_ica(deps, env), ContractState::IcaCreated => try_split_funds(deps, env), ContractState::Completed => { @@ -115,12 +119,12 @@ fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> NeutronResult NeutronResult> { +fn try_register_ica(deps: ExecuteDeps, env: Env) -> NeutronResult> { let remote_chain_info = REMOTE_CHAIN_INFO.load(deps.storage)?; let register: NeutronMsg = NeutronMsg::register_interchain_account( remote_chain_info.connection_id, INTERCHAIN_ACCOUNT_ID.to_string(), - None, + Some(vec![get_default_ica_fee()]), ); let key = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); @@ -132,69 +136,102 @@ fn try_register_ica(deps: DepsMut, env: Env) -> NeutronResult NeutronResult> { +fn try_split_funds(mut deps: ExecuteDeps, env: Env) -> NeutronResult> { let port_id = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); let interchain_account = INTERCHAIN_ACCOUNTS.load(deps.storage, port_id.clone())?; + let amount = TRANSFER_AMOUNT.load(deps.storage)?; match interchain_account { Some((address, controller_conn_id)) => { let remote_chain_info = REMOTE_CHAIN_INFO.load(deps.storage)?; - let amount = TRANSFER_AMOUNT.load(deps.storage)?; + let splits = SPLIT_CONFIG_MAP.load(deps.storage, remote_chain_info.denom.to_string())?; let mut outputs: Vec = Vec::with_capacity(splits.len()); for split_receiver in splits.iter() { + // query the ibc forwarders for their ICA addresses + // if either does not exist yet, error out + let forwarder_deposit_address: Option = deps.querier.query_wasm_smart( + split_receiver.addr.to_string(), + &covenant_utils::neutron_ica::CovenantQueryMsg::DepositAddress {}, + )?; + + let receiver_ica = match forwarder_deposit_address { + Some(ica) => ica, + None => { + return Err(NeutronError::Std(StdError::NotFound { + kind: "forwarder ica not created".to_string(), + })) + } + }; + // get the fraction dedicated to this receiver let amt = amount - .checked_multiply_ratio(split_receiver.share, Uint128::new(100)) - .map_err(|e| NeutronError::Std(StdError::GenericErr { msg: e.to_string() }))?; - - outputs.push(Output { - address: split_receiver.addr.to_string(), - coins: vec![Coin { - denom: remote_chain_info.denom.to_string(), - amount: amt.to_string(), - }], - }); + .checked_multiply_ratio( + split_receiver.share.numerator(), + split_receiver.share.denominator(), + ) + .map_err(|e: cosmwasm_std::CheckedMultiplyRatioError| { + NeutronError::Std(StdError::GenericErr { msg: e.to_string() }) + })?; + + let coin = Coin { + denom: remote_chain_info.denom.to_string(), + amount: amt.to_string(), + }; + let output = Output { + address: receiver_ica, + coins: vec![coin.clone()], + }; + + outputs.push(output); } - // todo: make sure output amounts add up to the input amount here - let multi_send_msg = MsgMultiSend { - inputs: vec![Input { - address, - coins: vec![Coin { - denom: remote_chain_info.denom, - amount: amount.to_string(), - }], + let mut inputs: Vec = Vec::new(); + let input = Input { + address: address.to_string(), + coins: vec![Coin { + denom: remote_chain_info.denom, + amount: amount.to_string(), }], - outputs, }; + inputs.push(input); + + let multi_send_msg = MsgMultiSend { inputs, outputs }; + + // Serialize the Delegate message. + let mut buf = Vec::new(); + buf.reserve(multi_send_msg.encoded_len()); - let protobuf = neutron_ica::to_proto_msg_multi_send(multi_send_msg)?; + if let Err(e) = multi_send_msg.encode(&mut buf) { + return Err(NeutronError::Std(StdError::generic_err(format!( + "Encode error: {}", + e + )))); + } - // wrap the protobuf of MsgTransfer into a message to be executed - // by our interchain account + let any_msg = ProtobufAny { + type_url: "/cosmos.bank.v1beta1.MsgMultiSend".to_string(), + value: Binary::from(buf), + }; let submit_msg = NeutronMsg::submit_tx( controller_conn_id, INTERCHAIN_ACCOUNT_ID.to_string(), - vec![protobuf], + vec![any_msg], "".to_string(), remote_chain_info.ica_timeout.u64(), remote_chain_info.ibc_fee, ); - let sudo_msg = msg_with_sudo_callback( - deps, + deps.branch(), submit_msg, SudoPayload { port_id, message: "split_funds_msg".to_string(), }, )?; - Ok(Response::default() - .add_submessage(sudo_msg) - .add_attribute("method", "try_execute_split_funds")) + Ok(Response::default().add_submessages(vec![sudo_msg])) } None => { // I can't think of a case of how we could end up here as `sudo_open_ack` @@ -208,9 +245,8 @@ fn try_split_funds(deps: DepsMut, env: Env) -> NeutronResult>, T>( - deps: DepsMut, + deps: ExecuteDeps, msg: C, payload: SudoPayload, ) -> StdResult> { @@ -219,7 +255,7 @@ fn msg_with_sudo_callback>, T>( } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> NeutronResult { +pub fn query(deps: QueryDeps, env: Env, msg: QueryMsg) -> NeutronResult { match msg { QueryMsg::ClockAddress {} => Ok(to_json_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), QueryMsg::ContractState {} => Ok(to_json_binary(&CONTRACT_STATE.may_load(deps.storage)?)?), @@ -247,7 +283,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> NeutronResult } } -fn query_deposit_address(deps: Deps, env: Env) -> Result, StdError> { +fn query_deposit_address(deps: QueryDeps, env: Env) -> Result, StdError> { let key = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); /* here we cover three possible cases: @@ -256,10 +292,9 @@ fn query_deposit_address(deps: Deps, env: Env) -> Result None - 3. ICA creation request hadn't been submitted yet -> None */ - match INTERCHAIN_ACCOUNTS.may_load(deps.storage, key)? { - Some(Some((addr, _))) => Ok(Some(addr)), // case 1 - _ => Ok(None), // cases 2 and 3 - } + INTERCHAIN_ACCOUNTS + .may_load(deps.storage, key) + .map(|entry| entry.flatten().map(|x| x.0)) } fn get_ica( @@ -275,20 +310,17 @@ fn get_ica( } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> StdResult { +pub fn sudo(deps: ExecuteDeps, env: Env, msg: SudoMsg) -> StdResult { deps.api .debug(format!("WASMDEBUG: sudo: received sudo msg: {msg:?}").as_str()); match msg { // For handling successful (non-error) acknowledgements. SudoMsg::Response { request, data } => sudo_response(deps, request, data), - // For handling error acknowledgements. SudoMsg::Error { request, details } => sudo_error(deps, request, details), - // For handling error timeouts. SudoMsg::Timeout { request } => sudo_timeout(deps, env, request), - // For handling successful registering of ICA SudoMsg::OpenAck { port_id, @@ -307,149 +339,6 @@ pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> StdResult { } } -// handler -fn sudo_open_ack( - deps: DepsMut, - _env: Env, - port_id: String, - _channel_id: String, - _counterparty_channel_id: String, - counterparty_version: String, -) -> StdResult { - // The version variable contains a JSON value with multiple fields, - // including the generated account address. - let parsed_version: Result = - serde_json_wasm::from_str(counterparty_version.as_str()); - - // get the parsed OpenAckVersion or return an error if we fail - let Ok(parsed_version) = parsed_version else { - return Err(StdError::generic_err("Can't parse counterparty_version")); - }; - - // Update the storage record associated with the interchain account. - INTERCHAIN_ACCOUNTS.save( - deps.storage, - port_id, - &Some(( - parsed_version.clone().address, - parsed_version.controller_connection_id, - )), - )?; - CONTRACT_STATE.save(deps.storage, &ContractState::IcaCreated)?; - - Ok(Response::default().add_attribute("method", "sudo_open_ack")) -} - -fn sudo_response(mut deps: DepsMut, request: RequestPacket, data: Binary) -> StdResult { - let clock_addr = CLOCK_ADDRESS.load(deps.storage)?; - - deps.api - .debug(format!("WASMDEBUG: sudo_response: sudo received: {request:?} {data:?}",).as_str()); - - let seq_id = request - .sequence - .ok_or_else(|| StdError::generic_err("sequence not found"))?; - - let channel_id = request - .source_channel - .ok_or_else(|| StdError::generic_err("channel_id not found"))?; - - let payload = read_sudo_payload(deps.storage, channel_id, seq_id).ok(); - if payload.is_none() { - let error_msg = "WASMDEBUG: Error: Unable to read sudo payload"; - deps.api.debug(error_msg); - add_error_to_queue(deps.storage, error_msg.to_string()); - return Ok(Response::default()); - } - - let parsed_data = decode_acknowledgement_response(data)?; - - // Iterate over the messages, parse them depending on their type & process them. - let mut item_types = vec![]; - let mut complete_msg = vec![]; - - for item in parsed_data { - let item_type = item.msg_type.as_str(); - item_types.push(item_type.to_string()); - match item_type { - "/cosmos.bank.v1beta1.MsgMultiSend" => { - decode_message_response(&item.data)?; - // TODO: look into if this successful decoding is enough to assume multi - // send was successful - complete_msg.push(ContractState::complete_and_dequeue( - deps.branch(), - clock_addr.as_str(), - )?) - } - _ => { - deps.api.debug( - format!("This type of acknowledgement is not implemented: {payload:?}") - .as_str(), - ); - } - } - } - - Ok(Response::default() - .add_messages(complete_msg) - .add_attribute("method", "sudo_response")) -} - -fn sudo_timeout(deps: DepsMut, _env: Env, request: RequestPacket) -> StdResult { - deps.api - .debug(format!("WASMDEBUG: sudo timeout request: {request:?}").as_str()); - - // revert the state to Instantiated to force re-creation of ICA - CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; - - // returning Ok as this is anticipated. channel is already closed. - Ok(Response::default()) -} - -fn sudo_error(deps: DepsMut, request: RequestPacket, details: String) -> StdResult { - deps.api - .debug(format!("WASMDEBUG: sudo error: {details}").as_str()); - deps.api - .debug(format!("WASMDEBUG: request packet: {request:?}").as_str()); - - // either of these errors will close the channel - request - .sequence - .ok_or_else(|| StdError::generic_err("sequence not found"))?; - - request - .source_channel - .ok_or_else(|| StdError::generic_err("channel_id not found"))?; - - Ok(Response::default().add_attribute("method", "sudo_error")) -} - -// prepare_sudo_payload is called from reply handler -// The method is used to extract sequence id and channel from SubmitTxResponse to -// process sudo payload defined in msg_with_sudo_callback later in Sudo handler. -// Such flow msg_with_sudo_callback() -> reply() -> prepare_sudo_payload() -> sudo() -// allows you "attach" some payload to your SubmitTx message -// and process this payload when an acknowledgement for the SubmitTx message -// is received in Sudo handler -fn _prepare_sudo_payload(mut deps: DepsMut, _env: Env, msg: Reply) -> StdResult { - let payload = read_reply_payload(deps.storage)?; - let resp: MsgSubmitTxResponse = serde_json_wasm::from_slice( - msg.result - .into_result() - .map_err(StdError::generic_err)? - .data - .ok_or_else(|| StdError::generic_err("no result"))? - .as_slice(), - ) - .map_err(|e| StdError::generic_err(format!("failed to parse response: {e:?}")))?; - deps.api - .debug(format!("WASMDEBUG: reply msg: {resp:?}").as_str()); - let seq_id = resp.sequence_id; - let channel_id = resp.channel; - save_sudo_payload(deps.branch().storage, channel_id, seq_id, payload)?; - Ok(Response::new()) -} - #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult { deps.api.debug("WASMDEBUG: migrate"); @@ -505,3 +394,16 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult MigrateMsg::UpdateCodeId { data: _ } => todo!(), } } + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: ExecuteDeps, env: Env, msg: Reply) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: reply msg: {msg:?}").as_str()); + match msg.id { + SUDO_PAYLOAD_REPLY_ID => prepare_sudo_payload(deps, env, msg), + _ => Err(StdError::generic_err(format!( + "unsupported reply message id {}", + msg.id + ))), + } +} diff --git a/contracts/native-splitter/src/lib.rs b/contracts/native-splitter/src/lib.rs index 3b47dbd4..db1ed601 100644 --- a/contracts/native-splitter/src/lib.rs +++ b/contracts/native-splitter/src/lib.rs @@ -6,6 +6,7 @@ pub mod contract; pub mod error; pub mod msg; pub mod state; +pub mod sudo; #[allow(clippy::unwrap_used)] #[cfg(test)] diff --git a/contracts/native-splitter/src/msg.rs b/contracts/native-splitter/src/msg.rs index 3c16bd64..375fe3a5 100644 --- a/contracts/native-splitter/src/msg.rs +++ b/contracts/native-splitter/src/msg.rs @@ -1,7 +1,9 @@ use std::fmt; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Attribute, Binary, DepsMut, StdError, Uint128, Uint64, WasmMsg}; +use cosmwasm_std::{ + to_json_binary, Addr, Attribute, Binary, Decimal, DepsMut, StdError, Uint128, Uint64, WasmMsg, +}; use covenant_clock::helpers::dequeue_msg; use covenant_macros::{ clocked, covenant_clock_address, covenant_deposit_address, covenant_ica_address, @@ -45,20 +47,73 @@ pub struct InstantiateMsg { pub ibc_transfer_timeout: Uint64, } +#[cw_serde] +pub struct PresetNativeSplitterFields { + pub code_id: u64, + pub label: String, + pub remote_chain_connection_id: String, + pub remote_chain_channel_id: String, + pub denom: String, + pub amount: Uint128, + pub ibc_fee: IbcFee, + pub ica_timeout: Uint64, + pub ibc_transfer_timeout: Uint64, +} + +impl PresetNativeSplitterFields { + pub fn to_instantiate_msg( + &self, + clock_address: String, + splits: Vec, + ) -> InstantiateMsg { + InstantiateMsg { + clock_address, + remote_chain_connection_id: self.remote_chain_connection_id.to_string(), + remote_chain_channel_id: self.remote_chain_channel_id.to_string(), + denom: self.denom.to_string(), + amount: self.amount, + splits, + ibc_fee: self.ibc_fee.clone(), + ica_timeout: self.ica_timeout, + ibc_transfer_timeout: self.ibc_transfer_timeout, + } + } + + pub fn to_instantiate2_msg( + &self, + admin_addr: String, + salt: Binary, + clock_address: String, + splits: Vec, + ) -> Result { + Ok(WasmMsg::Instantiate2 { + admin: Some(admin_addr), + code_id: self.code_id, + label: self.label.to_string(), + msg: to_json_binary(&self.to_instantiate_msg(clock_address, splits))?, + funds: vec![], + salt, + }) + } +} + #[cw_serde] pub struct NativeDenomSplit { /// denom to be distributed pub denom: String, /// denom receivers and their respective shares + // TODO: convert to map of ibc forwarder -> decimal? pub receivers: Vec, } impl NativeDenomSplit { pub fn validate(self) -> Result { // here we validate that all receiver shares add up to 100 (%) - let sum: Uint128 = self.receivers.iter().map(|r| r.share).sum(); + let mut total_share = Decimal::zero(); + + self.receivers.iter().for_each(|r| total_share += r.share); - if sum != Uint128::new(100) { + if total_share != Decimal::one() { Err(StdError::generic_err(format!( "failed to validate split config for denom: {}", self.denom @@ -83,8 +138,7 @@ pub struct SplitReceiver { /// address of the receiver on remote chain pub addr: String, /// percentage share that the address is entitled to - // TODO: convert to cw Fraction - pub share: Uint128, + pub share: Decimal, } impl fmt::Display for SplitReceiver { diff --git a/contracts/native-splitter/src/sudo.rs b/contracts/native-splitter/src/sudo.rs new file mode 100644 index 00000000..34ced4a3 --- /dev/null +++ b/contracts/native-splitter/src/sudo.rs @@ -0,0 +1,118 @@ +use cosmwasm_std::{Binary, DepsMut, Env, Reply, Response, StdError, StdResult}; +use covenant_utils::neutron_ica::OpenAckVersion; +use neutron_sdk::{ + bindings::{msg::MsgSubmitTxResponse, query::NeutronQuery}, + sudo::msg::RequestPacket, +}; + +use crate::{ + msg::ContractState, + state::{read_reply_payload, save_sudo_payload, CONTRACT_STATE, INTERCHAIN_ACCOUNTS}, +}; + +type ExecuteDeps<'a> = DepsMut<'a, NeutronQuery>; + +// handler +pub fn sudo_open_ack( + deps: ExecuteDeps, + _env: Env, + port_id: String, + _channel_id: String, + _counterparty_channel_id: String, + counterparty_version: String, +) -> StdResult { + // The version variable contains a JSON value with multiple fields, + // including the generated account address. + let parsed_version: Result = + serde_json_wasm::from_str(counterparty_version.as_str()); + + // get the parsed OpenAckVersion or return an error if we fail + let Ok(parsed_version) = parsed_version else { + return Err(StdError::generic_err("Can't parse counterparty_version")); + }; + + // Update the storage record associated with the interchain account. + INTERCHAIN_ACCOUNTS.save( + deps.storage, + port_id, + &Some(( + parsed_version.clone().address, + parsed_version.controller_connection_id, + )), + )?; + CONTRACT_STATE.save(deps.storage, &ContractState::IcaCreated)?; + + Ok(Response::default().add_attribute("method", "sudo_open_ack")) +} + +pub fn sudo_response( + deps: ExecuteDeps, + request: RequestPacket, + data: Binary, +) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: sudo_response: sudo received: {request:?} {data:?}").as_str()); + + // either of these errors will close the channel + request + .sequence + .ok_or_else(|| StdError::generic_err("sequence not found"))?; + + request + .source_channel + .ok_or_else(|| StdError::generic_err("channel_id not found"))?; + + Ok(Response::default().add_attribute("method", "sudo_response")) +} + +pub fn sudo_timeout(deps: ExecuteDeps, _env: Env, request: RequestPacket) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: sudo timeout request: {request:?}").as_str()); + + // revert the state to Instantiated to force re-creation of ICA + CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; + + // returning Ok as this is anticipated. channel is already closed. + Ok(Response::default()) +} + +pub fn sudo_error( + deps: ExecuteDeps, + request: RequestPacket, + details: String, +) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: sudo error: {details}").as_str()); + deps.api + .debug(format!("WASMDEBUG: request packet: {request:?}").as_str()); + + // either of these errors will close the channel + request + .sequence + .ok_or_else(|| StdError::generic_err("sequence not found"))?; + + request + .source_channel + .ok_or_else(|| StdError::generic_err("channel_id not found"))?; + + Ok(Response::default().add_attribute("method", "sudo_error")) +} + +pub fn prepare_sudo_payload(mut deps: ExecuteDeps, _env: Env, msg: Reply) -> StdResult { + let payload = read_reply_payload(deps.storage)?; + let resp: MsgSubmitTxResponse = serde_json_wasm::from_slice( + msg.result + .into_result() + .map_err(StdError::generic_err)? + .data + .ok_or_else(|| StdError::generic_err("no result"))? + .as_slice(), + ) + .map_err(|e| StdError::generic_err(format!("failed to parse response: {e:?}")))?; + deps.api + .debug(format!("WASMDEBUG: reply msg: {resp:?}").as_str()); + let seq_id = resp.sequence_id; + let channel_id = resp.channel; + save_sudo_payload(deps.branch().storage, channel_id, seq_id, payload)?; + Ok(Response::new()) +} diff --git a/contracts/single-party-pol-covenant/.cargo/config b/contracts/single-party-pol-covenant/.cargo/config new file mode 100644 index 00000000..5f6aa466 --- /dev/null +++ b/contracts/single-party-pol-covenant/.cargo/config @@ -0,0 +1,3 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +schema = "run --bin schema" diff --git a/contracts/single-party-pol-covenant/Cargo.toml b/contracts/single-party-pol-covenant/Cargo.toml new file mode 100644 index 00000000..e2f9b432 --- /dev/null +++ b/contracts/single-party-pol-covenant/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "covenant-single-party-pol" +edition = { workspace = true } +authors = ["benskey bekauz@protonmail.com", "Art3miX "] +description = "Single Party POL covenant" +license = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +exclude = [ + "contract.wasm", + "hash.txt", +] + +[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-storage-plus = { workspace = true } +cw-utils = { workspace = true } +cw2 = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } +sha2 = { workspace = true } +neutron-sdk = { workspace = true } +cosmos-sdk-proto = { workspace = true } +protobuf = { workspace = true } +schemars = { workspace = true } +serde-json-wasm = { workspace = true } +base64 = { workspace = true } +prost = { workspace = true } +prost-types = { workspace = true } +bech32 = { workspace = true } +covenant-clock = { workspace = true, features = ["library"] } +covenant-utils = { workspace = true } +covenant-ibc-forwarder = { workspace = true, features = ["library"] } +covenant-interchain-router = { workspace = true, features = ["library"] } +covenant-single-party-pol-holder = { workspace = true, features = ["library"] } +covenant-astroport-liquid-pooler = { workspace = true, features = ["library"] } +covenant-interchain-splitter = { workspace = true, features = ["library"] } +covenant-stride-liquid-staker = { workspace = true, features = ["library"] } +covenant-native-splitter = { workspace = true, features = ["library"] } +astroport = { workspace = true } + +[dev-dependencies] +cw-multi-test = { workspace = true } +anyhow = { workspace = true } +astroport = { workspace = true } +prost = { workspace = true } diff --git a/contracts/single-party-pol-covenant/README.md b/contracts/single-party-pol-covenant/README.md new file mode 100644 index 00000000..049f19b3 --- /dev/null +++ b/contracts/single-party-pol-covenant/README.md @@ -0,0 +1,3 @@ +# single party POL covenant + +TODO diff --git a/contracts/single-party-pol-covenant/src/contract.rs b/contracts/single-party-pol-covenant/src/contract.rs new file mode 100644 index 00000000..d0474ebd --- /dev/null +++ b/contracts/single-party-pol-covenant/src/contract.rs @@ -0,0 +1,411 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_json_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, WasmMsg, +}; + +use covenant_astroport_liquid_pooler::msg::{ + AssetData, PresetAstroLiquidPoolerFields, SingleSideLpLimits, +}; +use covenant_clock::msg::PresetClockFields; +use covenant_ibc_forwarder::msg::PresetIbcForwarderFields; +use covenant_native_splitter::msg::{NativeDenomSplit, PresetNativeSplitterFields, SplitReceiver}; +use covenant_single_party_pol_holder::msg::PresetHolderFields; +use covenant_stride_liquid_staker::msg::PresetStrideLsFields; +use covenant_utils::instantiate2_helper::get_instantiate2_salt_and_address; +use cw2::set_contract_version; + +use crate::{ + error::ContractError, + msg::{CovenantPartyConfig, InstantiateMsg, MigrateMsg, QueryMsg}, + state::{ + COVENANT_CLOCK_ADDR, HOLDER_ADDR, LIQUID_POOLER_ADDR, LIQUID_STAKER_ADDR, + LP_FORWARDER_ADDR, LS_FORWARDER_ADDR, PRESET_CLOCK_FIELDS, PRESET_HOLDER_FIELDS, + PRESET_LIQUID_POOLER_FIELDS, PRESET_LIQUID_STAKER_FIELDS, PRESET_LP_FORWARDER_FIELDS, + PRESET_LS_FORWARDER_FIELDS, PRESET_SPLITTER_FIELDS, SPLITTER_ADDR, + }, +}; + +const CONTRACT_NAME: &str = "crates.io:covenant-single-party-pol"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub const CLOCK_SALT: &[u8] = b"clock"; +pub const HOLDER_SALT: &[u8] = b"pol_holder"; +pub const NATIVE_SPLITTER_SALT: &[u8] = b"native_splitter"; + +pub const LS_FORWARDER_SALT: &[u8] = b"ls_forwarder"; +pub const LP_FORWARDER_SALT: &[u8] = b"lp_forwarder"; + +pub const LIQUID_POOLER_SALT: &[u8] = b"liquid_pooler"; +pub const LIQUID_STAKER_SALT: &[u8] = b"liquid_staker"; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + let creator_address = deps.api.addr_canonicalize(env.contract.address.as_str())?; + + let (clock_salt, clock_address) = get_instantiate2_salt_and_address( + deps.as_ref(), + CLOCK_SALT, + &creator_address, + msg.contract_codes.clock_code, + )?; + let (native_splitter_salt, splitter_address) = get_instantiate2_salt_and_address( + deps.as_ref(), + NATIVE_SPLITTER_SALT, + &creator_address, + msg.contract_codes.native_splitter_code, + )?; + let (ls_forwarder_salt, ls_forwarder_address) = get_instantiate2_salt_and_address( + deps.as_ref(), + LS_FORWARDER_SALT, + &creator_address, + msg.contract_codes.ibc_forwarder_code, + )?; + let (lp_forwarder_salt, lp_forwarder_address) = get_instantiate2_salt_and_address( + deps.as_ref(), + LP_FORWARDER_SALT, + &creator_address, + msg.contract_codes.ibc_forwarder_code, + )?; + let (liquid_staker_salt, liquid_staker_address) = get_instantiate2_salt_and_address( + deps.as_ref(), + LIQUID_STAKER_SALT, + &creator_address, + msg.contract_codes.liquid_staker_code, + )?; + let (liquid_pooler_salt, liquid_pooler_address) = get_instantiate2_salt_and_address( + deps.as_ref(), + LIQUID_POOLER_SALT, + &creator_address, + msg.contract_codes.liquid_pooler_code, + )?; + let (holder_salt, holder_address) = get_instantiate2_salt_and_address( + deps.as_ref(), + HOLDER_SALT, + &creator_address, + msg.contract_codes.holder_code, + )?; + + HOLDER_ADDR.save(deps.storage, &holder_address)?; + LIQUID_POOLER_ADDR.save(deps.storage, &liquid_pooler_address)?; + LIQUID_STAKER_ADDR.save(deps.storage, &liquid_staker_address)?; + COVENANT_CLOCK_ADDR.save(deps.storage, &clock_address)?; + SPLITTER_ADDR.save(deps.storage, &splitter_address)?; + + let mut clock_whitelist = Vec::with_capacity(7); + clock_whitelist.push(splitter_address.to_string()); + clock_whitelist.push(liquid_pooler_address.to_string()); + clock_whitelist.push(liquid_staker_address.to_string()); + clock_whitelist.push(holder_address.to_string()); + + let preset_ls_forwarder_fields = match msg.clone().ls_forwarder_config { + CovenantPartyConfig::Interchain(config) => { + LS_FORWARDER_ADDR.save(deps.storage, &ls_forwarder_address)?; + clock_whitelist.insert(0, ls_forwarder_address.to_string()); + + let preset = PresetIbcForwarderFields { + remote_chain_connection_id: config.party_chain_connection_id, + remote_chain_channel_id: config.party_to_host_chain_channel_id, + denom: config.remote_chain_denom, + amount: config.contribution.amount, + label: format!("{}_ls_ibc_forwarder", msg.label), + code_id: msg.contract_codes.ibc_forwarder_code, + ica_timeout: msg.timeouts.ica_timeout, + ibc_transfer_timeout: msg.timeouts.ibc_transfer_timeout, + ibc_fee: msg.preset_ibc_fee.to_ibc_fee(), + }; + PRESET_LS_FORWARDER_FIELDS.save(deps.storage, &preset)?; + + Some(preset) + } + CovenantPartyConfig::Native(_) => None, + }; + + let preset_lp_forwarder_fields = match msg.clone().lp_forwarder_config { + CovenantPartyConfig::Interchain(config) => { + LP_FORWARDER_ADDR.save(deps.storage, &lp_forwarder_address)?; + clock_whitelist.insert(0, lp_forwarder_address.to_string()); + + let preset = PresetIbcForwarderFields { + remote_chain_connection_id: config.party_chain_connection_id, + remote_chain_channel_id: config.party_to_host_chain_channel_id, + denom: config.remote_chain_denom, + amount: config.contribution.amount, + label: format!("{}_lp_ibc_forwarder", msg.label), + code_id: msg.contract_codes.ibc_forwarder_code, + ica_timeout: msg.timeouts.ica_timeout, + ibc_transfer_timeout: msg.timeouts.ibc_transfer_timeout, + ibc_fee: msg.preset_ibc_fee.to_ibc_fee(), + }; + PRESET_LP_FORWARDER_FIELDS.save(deps.storage, &preset)?; + + Some(preset) + } + CovenantPartyConfig::Native(_) => None, + }; + + let preset_clock_fields = PresetClockFields { + tick_max_gas: msg.clock_tick_max_gas, + whitelist: clock_whitelist, + code_id: msg.contract_codes.clock_code, + label: format!("{}-clock", msg.label), + }; + PRESET_CLOCK_FIELDS.save(deps.storage, &preset_clock_fields)?; + + // Holder + let preset_holder_fields = PresetHolderFields { + code_id: msg.contract_codes.holder_code, + label: format!("{}-holder", msg.label), + withdrawer: Some(info.sender.to_string()), + withdraw_to: Some(info.sender.to_string()), + lockup_period: msg.lockup_period, + }; + PRESET_HOLDER_FIELDS.save(deps.storage, &preset_holder_fields)?; + + // Liquid staker + let preset_liquid_staker_fields = PresetStrideLsFields { + label: format!("{}_stride_liquid_staker", msg.label), + ls_denom: msg.ls_info.ls_denom, + stride_neutron_ibc_transfer_channel_id: msg.ls_info.ls_chain_to_neutron_channel_id, + neutron_stride_ibc_connection_id: msg.ls_info.ls_neutron_connection_id, + ica_timeout: msg.timeouts.ica_timeout, + ibc_transfer_timeout: msg.timeouts.ibc_transfer_timeout, + ibc_fee: msg.preset_ibc_fee.to_ibc_fee(), + code_id: msg.contract_codes.liquid_staker_code, + }; + PRESET_LIQUID_STAKER_FIELDS.save(deps.storage, &preset_liquid_staker_fields)?; + + // Liquid pooler + let preset_liquid_pooler_fields = PresetAstroLiquidPoolerFields { + slippage_tolerance: None, + assets: AssetData { + asset_a_denom: msg.ls_info.ls_denom_on_neutron, + asset_b_denom: msg.lp_forwarder_config.get_native_denom(), + }, + single_side_lp_limits: SingleSideLpLimits { + asset_a_limit: msg.party_a_single_side_limit, + asset_b_limit: msg.party_b_single_side_limit, + }, + label: format!("{}_liquid_pooler", msg.label), + code_id: msg.contract_codes.liquid_pooler_code, + expected_pool_ratio: msg.expected_pool_ratio, + acceptable_pool_ratio_delta: msg.acceptable_pool_ratio_delta, + pair_type: msg.pool_pair_type, + }; + PRESET_LIQUID_POOLER_FIELDS.save(deps.storage, &preset_liquid_pooler_fields)?; + + let preset_splitter_fields = PresetNativeSplitterFields { + remote_chain_channel_id: msg.native_splitter_config.channel_id, + remote_chain_connection_id: msg.native_splitter_config.connection_id, + code_id: msg.contract_codes.native_splitter_code, + label: format!("{}_remote_chain_splitter", msg.label), + denom: msg.native_splitter_config.denom.to_string(), + amount: msg.native_splitter_config.amount, + ibc_fee: msg.preset_ibc_fee.to_ibc_fee(), + ica_timeout: msg.timeouts.ica_timeout, + ibc_transfer_timeout: msg.timeouts.ibc_transfer_timeout, + }; + PRESET_SPLITTER_FIELDS.save(deps.storage, &preset_splitter_fields)?; + + let mut messages = vec![ + preset_clock_fields.to_instantiate2_msg(env.contract.address.to_string(), clock_salt)?, + preset_liquid_staker_fields.to_instantiate2_msg( + env.contract.address.to_string(), + liquid_staker_salt, + clock_address.to_string(), + liquid_pooler_address.to_string(), + )?, + preset_holder_fields.to_instantiate2_msg( + env.contract.address.to_string(), + holder_salt, + liquid_pooler_address.to_string(), + )?, + preset_liquid_pooler_fields.to_instantiate2_msg( + env.contract.address.to_string(), + liquid_pooler_salt, + msg.pool_address, + clock_address.to_string(), + holder_address.to_string(), + )?, + preset_splitter_fields.to_instantiate2_msg( + env.contract.address.to_string(), + native_splitter_salt, + clock_address.to_string(), + vec![NativeDenomSplit { + denom: msg.native_splitter_config.denom.to_string(), + receivers: vec![ + SplitReceiver { + addr: ls_forwarder_address.to_string(), + share: msg.native_splitter_config.ls_share, + }, + SplitReceiver { + addr: lp_forwarder_address.to_string(), + share: msg.native_splitter_config.native_share, + }, + ], + }], + )?, + ]; + + if let Some(fields) = preset_ls_forwarder_fields { + messages.push(fields.to_instantiate2_msg( + env.contract.address.to_string(), + ls_forwarder_salt, + clock_address.to_string(), + liquid_staker_address.to_string(), + )?); + } + + if let Some(fields) = preset_lp_forwarder_fields { + messages.push(fields.to_instantiate2_msg( + env.contract.address.to_string(), + lp_forwarder_salt, + clock_address.to_string(), + liquid_pooler_address.to_string(), + )?); + }; + + Ok(Response::default() + .add_messages(messages) + .add_attribute("method", "instantiate")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::ClockAddress {} => Ok(to_json_binary( + &COVENANT_CLOCK_ADDR.may_load(deps.storage)?, + )?), + QueryMsg::HolderAddress {} => Ok(to_json_binary(&HOLDER_ADDR.may_load(deps.storage)?)?), + QueryMsg::IbcForwarderAddress { ty } => { + let resp = if ty == "lp" { + LP_FORWARDER_ADDR.may_load(deps.storage)? + } else if ty == "ls" { + LS_FORWARDER_ADDR.may_load(deps.storage)? + } else { + Some(Addr::unchecked("not found")) + }; + Ok(to_json_binary(&resp)?) + } + QueryMsg::LiquidStakerAddress {} => { + Ok(to_json_binary(&LIQUID_STAKER_ADDR.may_load(deps.storage)?)?) + } + QueryMsg::LiquidPoolerAddress {} => { + Ok(to_json_binary(&LIQUID_POOLER_ADDR.may_load(deps.storage)?)?) + } + QueryMsg::SplitterAddress {} => Ok(to_json_binary(&SPLITTER_ADDR.load(deps.storage)?)?), + QueryMsg::PartyDepositAddress {} => { + let splitter_address = SPLITTER_ADDR.load(deps.storage)?; + let ica: Option = deps.querier.query_wasm_smart( + splitter_address, + &covenant_utils::neutron_ica::CovenantQueryMsg::DepositAddress {}, + )?; + + Ok(to_json_binary(&ica)?) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult { + deps.api.debug("WASMDEBUG: migrate"); + match msg { + MigrateMsg::MigrateContracts { + clock, + ls_forwarder, + lp_forwarder, + holder, + liquid_pooler, + liquid_staker, + splitter, + } => { + let mut migrate_msgs = vec![]; + let mut resp = Response::default().add_attribute("method", "migrate_contracts"); + + if let Some(clock) = clock { + let msg = to_json_binary(&clock)?; + let clock_fields = PRESET_CLOCK_FIELDS.load(deps.storage)?; + resp = resp.add_attribute("clock_migrate", msg.to_base64()); + migrate_msgs.push(WasmMsg::Migrate { + contract_addr: COVENANT_CLOCK_ADDR.load(deps.storage)?.to_string(), + new_code_id: clock_fields.code_id, + msg, + }); + } + + if let Some(forwarder) = ls_forwarder { + let msg: Binary = to_json_binary(&forwarder)?; + let forwarder_fields = PRESET_LS_FORWARDER_FIELDS.load(deps.storage)?; + resp = resp.add_attribute("ls_forwarder_migrate", msg.to_base64()); + migrate_msgs.push(WasmMsg::Migrate { + contract_addr: LS_FORWARDER_ADDR.load(deps.storage)?.to_string(), + new_code_id: forwarder_fields.code_id, + msg, + }); + } + + if let Some(forwarder) = lp_forwarder { + let msg: Binary = to_json_binary(&forwarder)?; + let forwarder_fields = PRESET_LP_FORWARDER_FIELDS.load(deps.storage)?; + resp = resp.add_attribute("lp_forwarder_migrate", msg.to_base64()); + migrate_msgs.push(WasmMsg::Migrate { + contract_addr: LP_FORWARDER_ADDR.load(deps.storage)?.to_string(), + new_code_id: forwarder_fields.code_id, + msg, + }); + } + + if let Some(liquid_pooler) = liquid_pooler { + let msg: Binary = to_json_binary(&liquid_pooler)?; + let liquid_pooler_fields = PRESET_LIQUID_POOLER_FIELDS.load(deps.storage)?; + resp = resp.add_attribute("liquid_pooler_migrate", msg.to_base64()); + migrate_msgs.push(WasmMsg::Migrate { + contract_addr: LIQUID_POOLER_ADDR.load(deps.storage)?.to_string(), + new_code_id: liquid_pooler_fields.code_id, + msg, + }); + } + + if let Some(splitter) = splitter { + let msg: Binary = to_json_binary(&splitter)?; + let splitter_fields = PRESET_SPLITTER_FIELDS.load(deps.storage)?; + resp = resp.add_attribute("splitter_migrate", msg.to_base64()); + migrate_msgs.push(WasmMsg::Migrate { + contract_addr: SPLITTER_ADDR.load(deps.storage)?.to_string(), + new_code_id: splitter_fields.code_id, + msg, + }); + } + + if let Some(holder) = holder { + let msg: Binary = to_json_binary(&holder)?; + let holder_fields = PRESET_HOLDER_FIELDS.load(deps.storage)?; + resp = resp.add_attribute("holder_migrate", msg.to_base64()); + migrate_msgs.push(WasmMsg::Migrate { + contract_addr: HOLDER_ADDR.load(deps.storage)?.to_string(), + new_code_id: holder_fields.code_id, + msg, + }); + } + + if let Some(liquid_staker) = liquid_staker { + let msg: Binary = to_json_binary(&liquid_staker)?; + let liquid_staker_fields = PRESET_LIQUID_STAKER_FIELDS.load(deps.storage)?; + resp = resp.add_attribute("liquid_staker_migrate", msg.to_base64()); + migrate_msgs.push(WasmMsg::Migrate { + contract_addr: LIQUID_STAKER_ADDR.load(deps.storage)?.to_string(), + new_code_id: liquid_staker_fields.code_id, + msg, + }); + } + + Ok(resp.add_messages(migrate_msgs)) + } + } +} diff --git a/contracts/single-party-pol-covenant/src/error.rs b/contracts/single-party-pol-covenant/src/error.rs new file mode 100644 index 00000000..2b7b2d21 --- /dev/null +++ b/contracts/single-party-pol-covenant/src/error.rs @@ -0,0 +1,27 @@ +use cosmwasm_std::{Instantiate2AddressError, StdError}; +use cw_utils::ParseReplyError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Unknown reply id")] + UnknownReplyId {}, + + #[error("SubMsg reply error")] + ReplyError { err: String }, + + #[error("Failed to instantiate {contract:?} contract")] + ContractInstantiationError { + contract: String, + err: ParseReplyError, + }, + + #[error("{0}")] + InstantiationError(#[from] Instantiate2AddressError), +} diff --git a/contracts/single-party-pol-covenant/src/lib.rs b/contracts/single-party-pol-covenant/src/lib.rs new file mode 100644 index 00000000..0faea8f4 --- /dev/null +++ b/contracts/single-party-pol-covenant/src/lib.rs @@ -0,0 +1,8 @@ +#![warn(clippy::unwrap_used, clippy::expect_used)] + +extern crate core; + +pub mod contract; +pub mod error; +pub mod msg; +pub mod state; diff --git a/contracts/single-party-pol-covenant/src/msg.rs b/contracts/single-party-pol-covenant/src/msg.rs new file mode 100644 index 00000000..8abb7e34 --- /dev/null +++ b/contracts/single-party-pol-covenant/src/msg.rs @@ -0,0 +1,221 @@ +use astroport::factory::PairType; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Coin, Decimal, Uint128, Uint64}; +use covenant_utils::{CovenantParty, DestinationConfig, ReceiverConfig}; +use cw_utils::Expiration; +use neutron_sdk::bindings::msg::IbcFee; + +const NEUTRON_DENOM: &str = "untrn"; +pub const DEFAULT_TIMEOUT: u64 = 60 * 60 * 5; // 5 hours + +#[cw_serde] +pub struct InstantiateMsg { + pub label: String, + pub timeouts: Timeouts, + pub preset_ibc_fee: PresetIbcFee, + pub contract_codes: CovenantContractCodeIds, + pub clock_tick_max_gas: Option, + pub lockup_period: Expiration, + pub pool_address: String, + pub ls_info: LsInfo, + pub party_a_single_side_limit: Uint128, + pub party_b_single_side_limit: Uint128, + pub ls_forwarder_config: CovenantPartyConfig, + pub lp_forwarder_config: CovenantPartyConfig, + pub expected_pool_ratio: Decimal, + pub acceptable_pool_ratio_delta: Decimal, + pub pool_pair_type: PairType, + pub native_splitter_config: NativeSplitterConfig, +} + +#[cw_serde] +pub struct NativeSplitterConfig { + pub channel_id: String, + pub connection_id: String, + pub denom: String, + pub amount: Uint128, + pub ls_share: Decimal, + pub native_share: Decimal, +} + +#[cw_serde] +pub struct LsInfo { + pub ls_denom: String, + pub ls_denom_on_neutron: String, + pub ls_chain_to_neutron_channel_id: String, + pub ls_neutron_connection_id: String, +} + +impl CovenantPartyConfig { + pub fn to_receiver_config(&self) -> ReceiverConfig { + match self { + CovenantPartyConfig::Interchain(config) => ReceiverConfig::Ibc(DestinationConfig { + destination_chain_channel_id: config.host_to_party_chain_channel_id.to_string(), + destination_receiver_addr: config.party_receiver_addr.to_string(), + ibc_transfer_timeout: config.ibc_transfer_timeout, + }), + CovenantPartyConfig::Native(config) => { + ReceiverConfig::Native(Addr::unchecked(config.party_receiver_addr.to_string())) + } + } + } + + pub fn get_final_receiver_address(&self) -> String { + match self { + CovenantPartyConfig::Interchain(config) => config.party_receiver_addr.to_string(), + CovenantPartyConfig::Native(config) => config.party_receiver_addr.to_string(), + } + } + + pub fn to_covenant_party(&self) -> CovenantParty { + match self { + CovenantPartyConfig::Interchain(config) => CovenantParty { + addr: config.addr.to_string(), + native_denom: config.native_denom.to_string(), + receiver_config: self.to_receiver_config(), + }, + CovenantPartyConfig::Native(config) => CovenantParty { + addr: config.addr.to_string(), + native_denom: config.native_denom.to_string(), + receiver_config: self.to_receiver_config(), + }, + } + } + + pub fn get_native_denom(&self) -> String { + match self { + CovenantPartyConfig::Interchain(config) => config.native_denom.to_string(), + CovenantPartyConfig::Native(config) => config.native_denom.to_string(), + } + } +} + +#[cw_serde] +pub enum CovenantPartyConfig { + Interchain(InterchainCovenantParty), + Native(NativeCovenantParty), +} + +#[cw_serde] +pub struct NativeCovenantParty { + /// address of the receiver on destination chain + pub party_receiver_addr: String, + /// denom provided by the party on neutron + pub native_denom: String, + /// authorized address of the party on neutron + pub addr: String, + /// coin provided by the party on its native chain + pub contribution: Coin, +} + +#[cw_serde] +pub struct InterchainCovenantParty { + /// address of the receiver on destination chain + pub party_receiver_addr: String, + /// connection id to the party chain + pub party_chain_connection_id: String, + /// timeout in seconds + pub ibc_transfer_timeout: Uint64, + /// channel id from party to host chain + pub party_to_host_chain_channel_id: String, + /// channel id from host chain to the party chain + pub host_to_party_chain_channel_id: String, + /// denom provided by the party on its native chain + pub remote_chain_denom: String, + /// authorized address of the party on neutron + pub addr: String, + /// denom provided by the party on neutron + pub native_denom: String, + /// coin provided by the party on its native chain + pub contribution: Coin, +} + +#[cw_serde] +pub struct CovenantContractCodeIds { + pub ibc_forwarder_code: u64, + pub holder_code: u64, + pub clock_code: u64, + pub native_splitter_code: u64, + pub liquid_pooler_code: u64, + pub liquid_staker_code: u64, +} + +#[cw_serde] +pub struct Timeouts { + /// ica timeout in seconds + pub ica_timeout: Uint64, + /// ibc transfer timeout in seconds + pub ibc_transfer_timeout: Uint64, +} + +impl Default for Timeouts { + fn default() -> Self { + Self { + ica_timeout: Uint64::new(DEFAULT_TIMEOUT), + ibc_transfer_timeout: Uint64::new(DEFAULT_TIMEOUT), + } + } +} + +#[cw_serde] +pub struct PresetIbcFee { + pub ack_fee: Uint128, + pub timeout_fee: Uint128, +} + +impl PresetIbcFee { + pub fn to_ibc_fee(&self) -> IbcFee { + IbcFee { + // must be empty + recv_fee: vec![], + ack_fee: vec![cosmwasm_std::Coin { + denom: NEUTRON_DENOM.to_string(), + amount: self.ack_fee, + }], + timeout_fee: vec![cosmwasm_std::Coin { + denom: NEUTRON_DENOM.to_string(), + amount: self.timeout_fee, + }], + } + } +} + +#[cw_serde] +pub enum ExecuteMsg { + /// Withdraw from the LPer + Withdraw {}, + /// + Claim {}, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(Addr)] + ClockAddress {}, + #[returns(Addr)] + HolderAddress {}, + #[returns(Addr)] + IbcForwarderAddress { ty: String }, + #[returns(Addr)] + LiquidPoolerAddress {}, + #[returns(Addr)] + LiquidStakerAddress {}, + #[returns(Addr)] + SplitterAddress {}, + #[returns(Addr)] + PartyDepositAddress {}, +} + +#[cw_serde] +pub enum MigrateMsg { + MigrateContracts { + clock: Option, + holder: Option, + ls_forwarder: Option, + lp_forwarder: Option, + splitter: Option, + liquid_pooler: Option, + liquid_staker: Option, + }, +} diff --git a/contracts/single-party-pol-covenant/src/state.rs b/contracts/single-party-pol-covenant/src/state.rs new file mode 100644 index 00000000..14ddbe54 --- /dev/null +++ b/contracts/single-party-pol-covenant/src/state.rs @@ -0,0 +1,30 @@ +use cosmwasm_std::Addr; +use covenant_astroport_liquid_pooler::msg::PresetAstroLiquidPoolerFields; +use covenant_clock::msg::PresetClockFields; +use covenant_ibc_forwarder::msg::PresetIbcForwarderFields; + +use covenant_native_splitter::msg::PresetNativeSplitterFields; +use covenant_single_party_pol_holder::msg::PresetHolderFields; +use covenant_stride_liquid_staker::msg::PresetStrideLsFields; +use cw_storage_plus::Item; + +// fields related to the contracts known prior to their. +pub const PRESET_CLOCK_FIELDS: Item = Item::new("preset_clock_fields"); +pub const PRESET_HOLDER_FIELDS: Item = Item::new("preset_holder_fields"); +pub const PRESET_SPLITTER_FIELDS: Item = + Item::new("preset_splitter_fields"); +pub const PRESET_LS_FORWARDER_FIELDS: Item = + Item::new("preset_ls_forwarder_fields"); +pub const PRESET_LP_FORWARDER_FIELDS: Item = + Item::new("preset_lp_forwarder_fields"); +pub const PRESET_LIQUID_POOLER_FIELDS: Item = + Item::new("preset_lp_fields"); +pub const PRESET_LIQUID_STAKER_FIELDS: Item = Item::new("preset_ls_fields"); + +pub const COVENANT_CLOCK_ADDR: Item = Item::new("covenant_clock_addr"); +pub const HOLDER_ADDR: Item = Item::new("holder_addr"); +pub const SPLITTER_ADDR: Item = Item::new("remote_chain_splitter_addr"); +pub const LIQUID_POOLER_ADDR: Item = Item::new("liquid_pooler_addr"); +pub const LIQUID_STAKER_ADDR: Item = Item::new("liquid_staker_addr"); +pub const LS_FORWARDER_ADDR: Item = Item::new("ls_forwarder_addr"); +pub const LP_FORWARDER_ADDR: Item = Item::new("lp_forwarder_addr"); diff --git a/contracts/single-party-pol-holder/.cargo/config b/contracts/single-party-pol-holder/.cargo/config new file mode 100644 index 00000000..6fac702a --- /dev/null +++ b/contracts/single-party-pol-holder/.cargo/config @@ -0,0 +1,3 @@ +[alias] +wasm = "build --target wasm32-unknown-unknown --release" +wasm-debug = "build --target wasm32-unknown-unknown" \ No newline at end of file diff --git a/contracts/single-party-pol-holder/Cargo.toml b/contracts/single-party-pol-holder/Cargo.toml new file mode 100644 index 00000000..245537bd --- /dev/null +++ b/contracts/single-party-pol-holder/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "covenant-single-party-pol-holder" +authors = ["udit "] +description = "A holder can hold funds in a covenant" +edition = { workspace = true } +license = { workspace = true } +# rust-version = { workspace = true } +version = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# disables #[entry_point] (i.e. instantiate/execute/query) export +library = [] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } +cw-utils = { workspace = true } +covenant-macros = { workspace = true } +covenant-utils = { workspace = true } + +[dev-dependencies] +cw-multi-test = { workspace = true } +anyhow = { workspace = true } +astroport-token = { git = "https://github.com/astroport-fi/astroport-core.git" } +astroport-whitelist = { git = "https://github.com/astroport-fi/astroport-core.git" } +astroport-factory = { git = "https://github.com/astroport-fi/astroport-core.git" } +astroport-native-coin-registry = { git = "https://github.com/astroport-fi/astroport-core.git" } +astroport-pair-stable = { git = "https://github.com/astroport-fi/astroport-core.git" } diff --git a/contracts/single-party-pol-holder/README.md b/contracts/single-party-pol-holder/README.md new file mode 100644 index 00000000..f51aeda7 --- /dev/null +++ b/contracts/single-party-pol-holder/README.md @@ -0,0 +1 @@ +A single party holder mainly exists to withdraw the funds from the liquid pooler, it holds the logic of the distribution of the funds, and who can call the withdraw function. diff --git a/contracts/single-party-pol-holder/src/contract.rs b/contracts/single-party-pol-holder/src/contract.rs new file mode 100644 index 00000000..1ef9ce38 --- /dev/null +++ b/contracts/single-party-pol-holder/src/contract.rs @@ -0,0 +1,186 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + ensure, to_json_binary, BankMsg, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, + WasmMsg, +}; +use covenant_utils::withdraw_lp_helper::WithdrawLPMsgs; +use cw2::set_contract_version; + +use crate::error::ContractError; +use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use crate::state::{LOCKUP_PERIOD, POOLER_ADDRESS, WITHDRAWER, WITHDRAW_STATE, WITHDRAW_TO}; + +const CONTRACT_NAME: &str = "crates.io:covenant-holder"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + deps.api.debug("WASMDEBUG: holder instantiate"); + let mut resp = Response::default().add_attribute("method", "instantiate"); + + // withdrawer is optional on instantiation; can be set later + if let Some(addr) = msg.withdrawer { + WITHDRAWER.save(deps.storage, &deps.api.addr_validate(&addr)?)?; + resp = resp.add_attribute("withdrawer", addr); + }; + + if let Some(addr) = msg.withdraw_to { + WITHDRAW_TO.save(deps.storage, &deps.api.addr_validate(&addr)?)?; + resp = resp.add_attribute("withdraw_to", addr); + }; + + LOCKUP_PERIOD.save(deps.storage, &msg.lockup_period)?; + POOLER_ADDRESS.save(deps.storage, &deps.api.addr_validate(&msg.pooler_address)?)?; + + Ok(resp.add_attribute("pool_address", msg.pooler_address)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Withdrawer {} => Ok(to_json_binary(&WITHDRAWER.may_load(deps.storage)?)?), + QueryMsg::WithdrawTo {} => Ok(to_json_binary(&WITHDRAW_TO.may_load(deps.storage)?)?), + QueryMsg::PoolerAddress {} => Ok(to_json_binary(&POOLER_ADDRESS.may_load(deps.storage)?)?), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Claim {} => try_claim(deps, env, info), + ExecuteMsg::Distribute {} => try_distribute(deps, info), + ExecuteMsg::WithdrawFailed {} => try_withdraw_failed(deps, info), + } +} + +fn try_claim(deps: DepsMut, env: Env, info: MessageInfo) -> Result { + if WITHDRAW_STATE.load(deps.storage).is_ok() { + return Err(ContractError::WithdrawAlreadyStarted {}); + } + + let lockup_period = LOCKUP_PERIOD.load(deps.storage)?; + ensure!( + lockup_period.is_expired(&env.block), + ContractError::LockupPeriodNotOver(lockup_period.to_string()) + ); + + let withdrawer = WITHDRAWER + .load(deps.storage) + .map_err(|_| ContractError::NoWithdrawer {})?; + ensure!(info.sender == withdrawer, ContractError::Unauthorized {}); + + WITHDRAW_TO + .load(deps.storage) + .map_err(|_| ContractError::NoWithdrawTo {})?; + + let pooler_address = POOLER_ADDRESS.load(deps.storage)?; + + let withdraw_msg = WasmMsg::Execute { + contract_addr: pooler_address.to_string(), + msg: to_json_binary(&WithdrawLPMsgs::Withdraw { percentage: None })?, + funds: vec![], + }; + + WITHDRAW_STATE.save(deps.storage, &true)?; + + Ok(Response::default().add_message(withdraw_msg)) +} + +fn try_distribute(deps: DepsMut, info: MessageInfo) -> Result { + let pooler_addr = POOLER_ADDRESS.load(deps.storage)?; + ensure!(info.sender == pooler_addr, ContractError::Unauthorized {}); + + let withdraw_to_addr = WITHDRAW_TO + .load(deps.storage) + .map_err(|_| ContractError::NoWithdrawTo {})?; + + ensure!(info.funds.len() == 2, ContractError::InvalidFunds {}); + + WITHDRAW_STATE.remove(deps.storage); + + // TODO: Have a better logic to send funds over IBC + let send_msg = BankMsg::Send { + to_address: withdraw_to_addr.to_string(), + amount: info.funds, + }; + + Ok(Response::default().add_message(send_msg)) +} + +/// We don't need to do much if the withdraw failed. +/// We just need to ensure the caller is the pooler, and remove the withdraw_state storage +fn try_withdraw_failed(deps: DepsMut, info: MessageInfo) -> Result { + let pooler_addr = POOLER_ADDRESS.load(deps.storage)?; + ensure!(info.sender == pooler_addr, ContractError::Unauthorized {}); + + WITHDRAW_STATE.remove(deps.storage); + + Ok(Response::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result { + deps.api.debug("WASMDEBUG: migrate"); + + match msg { + MigrateMsg::UpdateConfig { + withdrawer, + withdraw_to, + pooler_address, + lockup_period, + } => { + let mut response = Response::default().add_attribute("method", "update_withdrawer"); + + if let Some(addr) = withdrawer { + WITHDRAWER.save(deps.storage, &deps.api.addr_validate(&addr)?)?; + response = response.add_attribute("withdrawer", addr); + } + + if let Some(addr) = withdraw_to { + WITHDRAW_TO.save(deps.storage, &deps.api.addr_validate(&addr)?)?; + response = response.add_attribute("withdraw_to", addr); + } + + if let Some(addr) = pooler_address { + POOLER_ADDRESS.save(deps.storage, &deps.api.addr_validate(&addr)?)?; + response = response.add_attribute("pool_address", addr); + } + + if let Some(expires) = lockup_period { + let curr_lockup = LOCKUP_PERIOD.load(deps.storage)?; + ensure!( + curr_lockup.is_expired(&env.block), + ContractError::LockupPeriodIsExpired {} + ); + + ensure!( + expires.is_expired(&env.block), + ContractError::MustBeFutureLockupPeriod {} + ); + + LOCKUP_PERIOD.save(deps.storage, &expires)?; + response = response.add_attribute("lockup_period", expires.to_string()); + } + + Ok(response) + } + MigrateMsg::UpdateCodeId { data: _ } => { + // This is a migrate message to update code id, + // Data is optional base64 that we can parse to any data we would like in the future + // let data: SomeStruct = from_binary(&data)?; + Ok(Response::default()) + } + } +} diff --git a/contracts/single-party-pol-holder/src/error.rs b/contracts/single-party-pol-holder/src/error.rs new file mode 100644 index 00000000..9b41107d --- /dev/null +++ b/contracts/single-party-pol-holder/src/error.rs @@ -0,0 +1,32 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("A withdraw process already started")] + WithdrawAlreadyStarted {}, + + #[error("No withdrawer address configured")] + NoWithdrawer {}, + + #[error("No withdraw_to address configured")] + NoWithdrawTo {}, + + #[error("The position is still locked, unlock at: {0}")] + LockupPeriodNotOver(String), + + #[error("The lockup period is already expired")] + LockupPeriodIsExpired, + + #[error("The lockup period must be in the future")] + MustBeFutureLockupPeriod, + + #[error("We exepct 2 denoms to be recieved by the pooler")] + InvalidFunds, +} diff --git a/contracts/single-party-pol-holder/src/lib.rs b/contracts/single-party-pol-holder/src/lib.rs new file mode 100644 index 00000000..20b2e3bb --- /dev/null +++ b/contracts/single-party-pol-holder/src/lib.rs @@ -0,0 +1,7 @@ +pub mod contract; +pub mod error; +pub mod msg; +pub mod state; + +#[cfg(test)] +mod suite_tests; diff --git a/contracts/single-party-pol-holder/src/msg.rs b/contracts/single-party-pol-holder/src/msg.rs new file mode 100644 index 00000000..1dad0aa9 --- /dev/null +++ b/contracts/single-party-pol-holder/src/msg.rs @@ -0,0 +1,92 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{to_json_binary, Addr, Binary, StdError, WasmMsg}; +use covenant_macros::covenant_holder_distribute; +use cw_utils::Expiration; + +#[cw_serde] +pub struct InstantiateMsg { + /// A withdrawer is the only authorized address that can withdraw + /// from the contract. + pub withdrawer: Option, + /// Withdraw the funds to this address + pub withdraw_to: Option, + /// the neutron address of the liquid pooler + pub pooler_address: String, + /// The lockup period for the covenant + pub lockup_period: Expiration, +} + +/// Preset fields are set by the user when instantiating the covenant. +/// use `to_instantiate_msg` implementation method to get `InstantiateMsg`. +#[cw_serde] +pub struct PresetHolderFields { + pub withdrawer: Option, + pub withdraw_to: Option, + pub lockup_period: Expiration, + pub code_id: u64, + pub label: String, +} + +impl PresetHolderFields { + /// takes in the `pool_address` from which the funds would be withdrawn + /// and returns an `InstantiateMsg`. + pub fn to_instantiate_msg(&self, pooler_address: String) -> InstantiateMsg { + InstantiateMsg { + withdrawer: self.withdrawer.clone(), + withdraw_to: self.withdraw_to.clone(), + pooler_address, + lockup_period: self.lockup_period, + } + } + + pub fn to_instantiate2_msg( + &self, + admin_addr: String, + salt: Binary, + pooler_address: String, + ) -> Result { + let instantiate_msg = self.to_instantiate_msg(pooler_address); + + Ok(WasmMsg::Instantiate2 { + admin: Some(admin_addr), + code_id: self.code_id, + label: self.label.to_string(), + msg: to_json_binary(&instantiate_msg)?, + funds: vec![], + salt, + }) + } +} + +#[covenant_holder_distribute] +#[cw_serde] +pub enum ExecuteMsg { + /// This is called by the withdrawer to start the withdraw process + Claim {}, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + // Queries the withdrawer address + #[returns(Option)] + Withdrawer {}, + #[returns(Option)] + WithdrawTo {}, + // Queries the pooler address + #[returns(Addr)] + PoolerAddress {}, +} + +#[cw_serde] +pub enum MigrateMsg { + UpdateConfig { + withdrawer: Option, + withdraw_to: Option, + pooler_address: Option, + lockup_period: Option, + }, + UpdateCodeId { + data: Option, + }, +} diff --git a/contracts/single-party-pol-holder/src/state.rs b/contracts/single-party-pol-holder/src/state.rs new file mode 100644 index 00000000..9d033c57 --- /dev/null +++ b/contracts/single-party-pol-holder/src/state.rs @@ -0,0 +1,22 @@ +use cosmwasm_std::Addr; +use cw_storage_plus::Item; +use cw_utils::Expiration; + +/// address authorized to withdraw liquidity and the underlying assets +pub const WITHDRAWER: Item = Item::new("withdrawer"); +/// Addr that we withdraw the liquidity to +pub const WITHDRAW_TO: Item = Item::new("withdraw_to"); +/// address of the pool we expect to withdraw assets from +pub const POOLER_ADDRESS: Item = Item::new("pool_address"); +/// The lockup period of the LP tokens +pub const LOCKUP_PERIOD: Item = Item::new("lockup_period"); +/// The state of the withdraw process +pub const WITHDRAW_STATE: Item = Item::new("withdraw_state"); + +// /// The state of a withdraw process +// /// When a claim is called, we sett the storage with `WithdrawState::Processing` +// /// We remove the state from storage when the withdraw is done or if it failed +// #[cw_serde] +// pub enum WithdrawState { +// Processing {}, +// } diff --git a/contracts/single-party-pol-holder/src/suite_tests/mod.rs b/contracts/single-party-pol-holder/src/suite_tests/mod.rs new file mode 100644 index 00000000..1cc9bdf2 --- /dev/null +++ b/contracts/single-party-pol-holder/src/suite_tests/mod.rs @@ -0,0 +1,24 @@ +// use cosmwasm_std::Empty; +// use cw_multi_test::{Contract, ContractWrapper}; + +mod suite; +mod tests; + +// Advantage to using a macro for this is that the error trace links +// to the exact line that the error occured, instead of inside of a +// function where the assertion would otherwise happen. +// macro_rules! is_error { +// ($x:expr, $e:expr) => { +// assert!(format!("{:#}", $x).contains($e)) +// }; +// } +// pub(crate) use is_error; + +// pub fn holder_contract() -> Box> { +// let contract = ContractWrapper::new( +// crate::contract::execute, +// crate::contract::instantiate, +// crate::contract::query, +// ); +// Box::new(contract) +// } diff --git a/contracts/single-party-pol-holder/src/suite_tests/suite.rs b/contracts/single-party-pol-holder/src/suite_tests/suite.rs new file mode 100644 index 00000000..fc65d9e1 --- /dev/null +++ b/contracts/single-party-pol-holder/src/suite_tests/suite.rs @@ -0,0 +1,151 @@ +// use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +// use cosmwasm_std::{Addr, Coin}; +// use cw_multi_test::{App, AppResponse, Executor}; + +// use super::holder_contract; + +// const ADMIN: &str = "admin"; +// pub const DEFAULT_WITHDRAWER: &str = "authorizedwithdrawer"; + +// pub struct Suite { +// pub app: App, +// pub holder: Addr, +// pub admin: Addr, +// pub holder_code_id: u64, +// pub pool_address: String, +// } + +// pub struct SuiteBuilder { +// pub instantiate: InstantiateMsg, +// pub app: App, +// } + +// impl Default for SuiteBuilder { +// fn default() -> Self { +// Self { +// instantiate: InstantiateMsg { +// withdrawer: Some(DEFAULT_WITHDRAWER.to_string()), +// pool_address: "stablepairpool".to_string(), +// }, +// app: App::default(), +// } +// } +// } + +// impl SuiteBuilder { +// pub fn with_withdrawer(mut self, addr: Option) -> Self { +// self.instantiate.withdrawer = addr; +// self +// } + +// pub fn with_pool(mut self, addr: String) -> Self { +// self.instantiate.pool_address = addr; +// self +// } + +// pub fn build(self) -> Suite { +// let mut app = self.app; +// let holder_code = app.store_code(holder_contract()); +// let holder = app +// .instantiate_contract( +// holder_code, +// Addr::unchecked(ADMIN), +// &self.instantiate, +// &[], +// "holder", +// Some(ADMIN.to_string()), +// ) +// .unwrap(); +// Suite { +// app, +// holder, +// admin: Addr::unchecked(ADMIN), +// holder_code_id: holder_code, +// pool_address: self.instantiate.pool_address, +// } +// } +// } + +// // actions +// impl Suite { +// pub fn withdraw_liquidity(&mut self, caller: &str) -> AppResponse { +// self.app +// .execute_contract( +// Addr::unchecked(caller), +// self.holder.clone(), +// &ExecuteMsg::WithdrawLiquidity {}, +// &[], +// ) +// .unwrap() +// } + +// /// sends a message on caller's behalf to withdraw a specified amount of tokens +// pub fn withdraw_tokens(&mut self, caller: &str, quantity: Vec) -> AppResponse { +// self.app +// .execute_contract( +// Addr::unchecked(caller), +// self.holder.clone(), +// &ExecuteMsg::Withdraw { +// quantity: Some(quantity), +// }, +// &[], +// ) +// .unwrap() +// } + +// /// sends a message on caller's behalf to withdraw remaining balance +// pub fn withdraw_all(&mut self, caller: &str) -> anyhow::Result { +// self.app.execute_contract( +// Addr::unchecked(caller), +// self.holder.clone(), +// &ExecuteMsg::Withdraw { quantity: None }, +// &[], +// ) +// } +// } + +// // queries +// impl Suite { +// pub fn query_withdrawer(&self) -> Addr { +// self.app +// .wrap() +// .query_wasm_smart(&self.holder, &QueryMsg::Withdrawer {}) +// .unwrap() +// } +// } + +// // helper +// impl Suite { +// pub fn fund_holder(&mut self, tokens: Vec) -> AppResponse { +// self.app +// .sudo(cw_multi_test::SudoMsg::Bank( +// cw_multi_test::BankSudo::Mint { +// to_address: self.holder.to_string(), +// amount: tokens, +// }, +// )) +// .unwrap() +// } + +// pub fn assert_holder_balance(&mut self, tokens: Vec) { +// for c in &tokens { +// let queried_amount = self +// .app +// .wrap() +// .query_balance(self.holder.to_string(), c.denom.clone()) +// .unwrap(); +// assert_eq!(&queried_amount, c); +// } +// } + +// pub fn assert_withdrawer_balance(&mut self, tokens: Vec) { +// for c in &tokens { +// let queried_amount = self +// .app +// .wrap() +// .query_balance(DEFAULT_WITHDRAWER.to_string(), c.denom.clone()) +// .unwrap(); +// assert_eq!(&queried_amount, c); +// } +// } +// } diff --git a/contracts/single-party-pol-holder/src/suite_tests/tests.rs b/contracts/single-party-pol-holder/src/suite_tests/tests.rs new file mode 100644 index 00000000..d7a61d69 --- /dev/null +++ b/contracts/single-party-pol-holder/src/suite_tests/tests.rs @@ -0,0 +1,141 @@ +// use super::suite::{SuiteBuilder, DEFAULT_WITHDRAWER}; +// use cosmwasm_std::{coin, coins, Addr}; + +// #[test] +// fn test_instantiate_and_query_withdrawer() { +// let suite = SuiteBuilder::default().build(); +// assert_eq!( +// suite.query_withdrawer(), +// Addr::unchecked(DEFAULT_WITHDRAWER.to_string()) +// ); +// } + +// #[test] +// #[should_panic(expected = "Invalid input: address not normalized")] +// fn test_instantiate_invalid_withdrawer() { +// SuiteBuilder::default() +// .with_withdrawer(Some("0Oo0Oo".to_string())) +// .build(); +// } + +// #[test] +// #[should_panic(expected = "Invalid input: address not normalized")] +// fn test_instantiate_invalid_lp_addr() { +// SuiteBuilder::default() +// .with_pool("0Oo0Oo".to_string()) +// .build(); +// } + +// #[test] +// #[should_panic(expected = "Unauthorized")] +// fn test_withdraw_all_unauthorized() { +// let mut suite = SuiteBuilder::default().build(); + +// suite.fund_holder(coins(100, "coin")); + +// // attacker attempts to withdraw, panic +// suite.withdraw_all("attacker").unwrap(); +// } + +// #[test] +// fn test_withdraw_all_single_denom() { +// let mut suite = SuiteBuilder::default().build(); + +// suite.fund_holder(coins(100, "coin")); + +// // withdraw all +// suite.withdraw_all(DEFAULT_WITHDRAWER).unwrap(); + +// // check to see there is no balance +// suite.assert_holder_balance(coins(0, "coin")); + +// // and withdrawer has them all +// suite.assert_withdrawer_balance(coins(100, "coin")); +// } + +// #[test] +// fn test_withdraw_all_two_denoms() { +// let mut suite = SuiteBuilder::default().build(); + +// let balances = vec![coin(80, "atom"), coin(70, "statom")]; +// suite.fund_holder(balances.clone()); + +// // withdraw all +// suite.withdraw_all(DEFAULT_WITHDRAWER).unwrap(); + +// // assert all funds are now in withdrawer address +// suite.assert_holder_balance(vec![coin(0, "atom"), coin(0, "statom")]); +// suite.assert_withdrawer_balance(balances); +// } + +// #[test] +// fn test_fund_single_withdraw_partial_single_denom() { +// let mut suite = SuiteBuilder::default().build(); + +// suite.fund_holder(vec![coin(80, "atom")]); + +// // withdraw 75 out of a total of 100 tokens +// suite.withdraw_tokens(DEFAULT_WITHDRAWER, coins(75, "atom")); + +// // check to see there are 25 tokens left in contract +// suite.assert_holder_balance(coins(5, "atom")); + +// // and holder has received 75 +// suite.assert_withdrawer_balance(coins(75, "atom")); +// } +// #[test] +// fn test_fund_multi_denom_withdraw_partial_two_denom() { +// let mut suite = SuiteBuilder::default().build(); + +// let balances = vec![coin(80, "atom"), coin(70, "statom")]; +// suite.fund_holder(balances); + +// let amt_to_withdraw = vec![coin(50, "atom"), coin(30, "statom")]; + +// suite.withdraw_tokens(DEFAULT_WITHDRAWER, amt_to_withdraw.clone()); + +// let expected_balance = vec![coin(30, "atom"), coin(40, "statom")]; +// suite.assert_holder_balance(expected_balance); +// suite.assert_withdrawer_balance(amt_to_withdraw); +// } + +// #[test] +// fn test_fund_multi_denom_withdraw_exact_single_denom() { +// let mut suite = SuiteBuilder::default().build(); + +// let balances = vec![coin(80, "atom"), coin(70, "stuatom")]; +// suite.fund_holder(balances); + +// suite.withdraw_tokens(DEFAULT_WITHDRAWER, coins(70, "stuatom")); + +// // check to see there are 0 tokens left +// suite.assert_holder_balance(vec![coin(80, "atom")]); + +// suite.assert_withdrawer_balance(coins(70, "stuatom")); +// } + +// #[test] +// #[should_panic(expected = "Cannot Sub with 70 and 100")] +// fn test_fund_single_and_withdraw_too_big_single_denom() { +// let mut suite = SuiteBuilder::default().build(); +// let holder_balances = vec![coin(80, "atom"), coin(70, "statom")]; +// suite.fund_holder(holder_balances); + +// suite.withdraw_tokens(DEFAULT_WITHDRAWER, coins(100, "statom")); +// } + +// #[test] +// #[should_panic(expected = "No withdrawer address configured")] +// fn test_withdraw_liquidity_no_withdrawer() { +// let mut suite = SuiteBuilder::default().with_withdrawer(None).build(); + +// suite.withdraw_liquidity(DEFAULT_WITHDRAWER); +// } + +// #[test] +// #[should_panic(expected = "No withdrawer address configured")] +// fn test_withdraw_balances_no_withdrawer() { +// let mut suite = SuiteBuilder::default().with_withdrawer(None).build(); + +// suite.withdraw_tokens(DEFAULT_WITHDRAWER, coins(100, "statom")); +// } diff --git a/contracts/stride-liquid-staker/.cargo/config b/contracts/stride-liquid-staker/.cargo/config new file mode 100644 index 00000000..5f6aa466 --- /dev/null +++ b/contracts/stride-liquid-staker/.cargo/config @@ -0,0 +1,3 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +schema = "run --bin schema" diff --git a/contracts/stride-liquid-staker/Cargo.toml b/contracts/stride-liquid-staker/Cargo.toml new file mode 100644 index 00000000..5aedcf64 --- /dev/null +++ b/contracts/stride-liquid-staker/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "covenant-stride-liquid-staker" +authors = ["benskey bekauz@protonmail.com", "Art3mix "] +description = "Liquid Staker module for stride covenant" +edition = { workspace = true } +license = { workspace = true } +# rust-version = { workspace = true } +version = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# disables #[entry_point] (i.e. instantiate/execute/query) export +library = [] + +[dependencies] +covenant-macros = { workspace = true } +covenant-clock = { workspace = true, features = ["library"] } +covenant-utils = { workspace = true } +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +thiserror = { workspace = true } +schemars = { workspace = true } +serde-json-wasm = { workspace = true } +serde = { workspace = true } +neutron-sdk = { workspace = true } +cosmos-sdk-proto = { workspace = true } +protobuf = { workspace = true } diff --git a/contracts/stride-liquid-staker/README.md b/contracts/stride-liquid-staker/README.md new file mode 100644 index 00000000..50fe6c0b --- /dev/null +++ b/contracts/stride-liquid-staker/README.md @@ -0,0 +1,4 @@ +The Ls creates an Interchain Account on a host chain that temporarily holds funds. It allows anyone to permissionlessly forward funds denominated in a preset token to a preset destination address with an IBC transfer. See `src/msg.rs` for the API. + +## Example usecase +The current intended usescase is to create a covenant controlled Interchain Account on Stride. The covenant plans to liquid stake Atom using Stride's Autopilot 1-click liquid stake feature. Stride's Autopilot feature enables IBC transfers to a receiving address on Stride to be automatically liquid staked and also for these liquid staked vouchers to optionally be forwarded over IBC to a destination address. The current use of the contract is to register the receiving address as an ICA on Stride and allow anybody to forward liquid staked Atom from that ICA to the LPer contract. The benefit here is that if Stride's Autopilot IBC forwarding is disabled or otherwise fails, any user can recover the funds by forwarding them to the LPer. \ No newline at end of file diff --git a/contracts/stride-liquid-staker/examples/schema.rs b/contracts/stride-liquid-staker/examples/schema.rs new file mode 100644 index 00000000..7699f08b --- /dev/null +++ b/contracts/stride-liquid-staker/examples/schema.rs @@ -0,0 +1,11 @@ +use cosmwasm_schema::write_api; +use covenant_stride_liquid_staker::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + migrate: MigrateMsg, + } +} diff --git a/contracts/stride-liquid-staker/json.json b/contracts/stride-liquid-staker/json.json new file mode 100644 index 00000000..683b2fbc --- /dev/null +++ b/contracts/stride-liquid-staker/json.json @@ -0,0 +1,10 @@ +{ + "autopilot": { + "receiver": "stride123", + "stakeibc": { + "action": "LiquidStake", + "ibc_receiver": "neutron123", + "transfer_channel": "STRIDE_NEUTRON_CHANNEL" + } + } +} diff --git a/contracts/stride-liquid-staker/src/contract.rs b/contracts/stride-liquid-staker/src/contract.rs new file mode 100644 index 00000000..b9c8d24c --- /dev/null +++ b/contracts/stride-liquid-staker/src/contract.rs @@ -0,0 +1,481 @@ +use cosmos_sdk_proto::ibc::applications::transfer::v1::MsgTransfer; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + coins, to_json_binary, to_json_string, Binary, Coin, CosmosMsg, CustomQuery, Deps, DepsMut, + Env, MessageInfo, Reply, Response, StdError, StdResult, SubMsg, Uint128, +}; +use covenant_clock::helpers::{enqueue_msg, verify_clock}; +use covenant_utils::neutron_ica::{ + self, get_proto_coin, OpenAckVersion, RemoteChainInfo, SudoPayload, +}; +use cw2::set_contract_version; + +use crate::helpers::{Autopilot, AutopilotConfig}; +use crate::msg::{ContractState, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use crate::state::{ + read_reply_payload, save_reply_payload, save_sudo_payload, CLOCK_ADDRESS, CONTRACT_STATE, + INTERCHAIN_ACCOUNTS, NEXT_CONTRACT, REMOTE_CHAIN_INFO, +}; +use neutron_sdk::{ + bindings::{ + msg::{MsgSubmitTxResponse, NeutronMsg}, + query::NeutronQuery, + }, + interchain_txs::helpers::get_port_id, + sudo::msg::{RequestPacket, SudoMsg}, + NeutronError, NeutronResult, +}; + +const INTERCHAIN_ACCOUNT_ID: &str = "stride-ica"; + +const CONTRACT_NAME: &str = "crates.io:covenant-stride-liquid-staker"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +const SUDO_PAYLOAD_REPLY_ID: u64 = 1u64; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> NeutronResult> { + deps.api.debug("WASMDEBUG: instantiate"); + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + // validate the addresses + let clock_addr = deps.api.addr_validate(&msg.clock_address)?; + let next_contract = deps.api.addr_validate(&msg.next_contract)?; + + CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; + NEXT_CONTRACT.save(deps.storage, &next_contract)?; + let remote_chain_info = RemoteChainInfo { + connection_id: msg.neutron_stride_ibc_connection_id, + channel_id: msg.stride_neutron_ibc_transfer_channel_id, + denom: msg.ls_denom, + ibc_transfer_timeout: msg.ibc_transfer_timeout, + ica_timeout: msg.ica_timeout, + ibc_fee: msg.ibc_fee, + }; + REMOTE_CHAIN_INFO.save(deps.storage, &remote_chain_info)?; + CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; + + Ok(Response::default() + .add_message(enqueue_msg(clock_addr.as_str())?) + .add_attribute("method", "ls_instantiate") + .add_attribute("clock_address", clock_addr) + .add_attribute("next_contract", next_contract) + .add_attributes(remote_chain_info.get_response_attributes())) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> NeutronResult> { + deps.api + .debug(format!("WASMDEBUG: execute: received msg: {msg:?}").as_str()); + match msg { + ExecuteMsg::Tick {} => try_tick(deps, env, info), + ExecuteMsg::Transfer { amount } => { + let ica_address = get_ica(deps.as_ref(), &env, INTERCHAIN_ACCOUNT_ID); + match ica_address { + Ok(_) => try_execute_transfer(deps, env, info, amount), + Err(_) => Ok(Response::default() + .add_attribute("method", "try_permisionless_transfer") + .add_attribute("ica_status", "not_created")), + } + } + } +} + +/// attempts to advance the state machine. performs `info.sender` validation +fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> NeutronResult> { + // Verify caller is the clock + verify_clock(&info.sender, &CLOCK_ADDRESS.load(deps.storage)?)?; + + let current_state = CONTRACT_STATE.load(deps.storage)?; + match current_state { + ContractState::Instantiated => try_register_stride_ica(deps, env), + ContractState::IcaCreated => Ok(Response::default()), + } +} + +/// registers an interchain account on stride with port_id associated with `INTERCHAIN_ACCOUNT_ID` +fn try_register_stride_ica(deps: DepsMut, env: Env) -> NeutronResult> { + let remote_chain_info = REMOTE_CHAIN_INFO.load(deps.storage)?; + let register_fee: Option> = Some(coins(1000001, "untrn")); + let register: NeutronMsg = NeutronMsg::register_interchain_account( + remote_chain_info.connection_id, + INTERCHAIN_ACCOUNT_ID.to_string(), + register_fee, + ); + let key = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); + + // we are saving empty data here because we handle response of registering ICA in sudo_open_ack method + INTERCHAIN_ACCOUNTS.save(deps.storage, key, &None)?; + + Ok(Response::new() + .add_attribute("method", "try_register_stride_ica") + .add_message(register)) +} + +/// this is a permisionless transfer method. once liquid staked funds are in this +/// contract, anyone can call this method by passing an amount (`Uint128`) to transfer +/// the funds (with `ls_denom`) to the liquid pooler module. +fn try_execute_transfer( + deps: DepsMut, + env: Env, + _info: MessageInfo, + amount: Uint128, +) -> NeutronResult> { + // first we verify whether the next contract is ready for receiving the funds + let next_contract = NEXT_CONTRACT.load(deps.storage)?; + let deposit_address_query = deps.querier.query_wasm_smart( + next_contract, + &covenant_utils::neutron_ica::QueryMsg::DepositAddress {}, + )?; + + // if query returns None, then we error and wait + let Some(deposit_address) = deposit_address_query else { + return Err(NeutronError::Std( + StdError::not_found("Next contract is not ready for receiving the funds yet") + )) + }; + + let port_id = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); + let interchain_account = INTERCHAIN_ACCOUNTS.load(deps.storage, port_id.clone())?; + + match interchain_account { + Some((address, controller_conn_id)) => { + let remote_chain_info = REMOTE_CHAIN_INFO.load(deps.storage)?; + + // inner MsgTransfer that will be sent from stride to neutron. + // because of this message delivery depending on the ica wrapper below, + // timeout_timestamp = current block + ica timeout + ibc_transfer_timeout + let msg = MsgTransfer { + source_port: "transfer".to_string(), + source_channel: remote_chain_info.channel_id, + token: Some(get_proto_coin(remote_chain_info.denom, amount)), + sender: address, + receiver: deposit_address, + timeout_height: None, + timeout_timestamp: env + .block + .time + .plus_seconds(remote_chain_info.ica_timeout.u64()) + .plus_seconds(remote_chain_info.ibc_transfer_timeout.u64()) + .nanos(), + }; + + let protobuf = neutron_ica::to_proto_msg_transfer(msg)?; + + // wrap the protobuf of MsgTransfer into a message to be executed + // by our interchain account + let submit_msg = NeutronMsg::submit_tx( + controller_conn_id, + INTERCHAIN_ACCOUNT_ID.to_string(), + vec![protobuf], + "".to_string(), + remote_chain_info.ica_timeout.u64(), + remote_chain_info.ibc_fee, + ); + + let sudo_msg = msg_with_sudo_callback( + deps, + submit_msg, + SudoPayload { + port_id, + message: "permisionless_transfer".to_string(), + }, + )?; + Ok(Response::default() + .add_submessage(sudo_msg) + .add_attribute("method", "try_execute_transfer")) + } + None => { + // I can't think of a case of how we could end up here as `sudo_open_ack` + // callback advances the state to `ICACreated` and stores the ICA. + // just in case, we revert the state to `Instantiated` to restart the flow. + CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; + Ok(Response::default() + .add_attribute("method", "try_execute_transfer") + .add_attribute("error", "no_ica_found")) + } + } +} + +#[allow(unused)] +fn msg_with_sudo_callback>, T>( + deps: DepsMut, + msg: C, + payload: SudoPayload, +) -> StdResult> { + save_reply_payload(deps.storage, payload)?; + Ok(SubMsg::reply_on_success(msg, SUDO_PAYLOAD_REPLY_ID)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> NeutronResult { + match msg { + QueryMsg::ClockAddress {} => Ok(to_json_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), + QueryMsg::IcaAddress {} => Ok(to_json_binary( + &get_ica(deps, &env, INTERCHAIN_ACCOUNT_ID)?.0, + )?), + QueryMsg::ContractState {} => Ok(to_json_binary(&CONTRACT_STATE.may_load(deps.storage)?)?), + QueryMsg::DepositAddress {} => { + let ica = get_ica(deps, &env, INTERCHAIN_ACCOUNT_ID)?.0; + + let autopilot = Autopilot { + autopilot: AutopilotConfig { + receiver: ica.to_string(), + stakeibc: crate::helpers::Stakeibc { + action: "LiquidStake".to_string(), + stride_address: ica, + }, + }, + }; + + let autopilot_str = to_json_string(&autopilot)?; + + Ok(to_json_binary(&autopilot_str)?) + } + QueryMsg::RemoteChainInfo {} => { + Ok(to_json_binary(&REMOTE_CHAIN_INFO.may_load(deps.storage)?)?) + } + QueryMsg::NextMemo {} => { + // let next_contract = NEXT_CONTRACT.load(deps.storage)?; + // 1. receiver = query ICA + let ica = get_ica(deps, &env, INTERCHAIN_ACCOUNT_ID)?.0; + + let autopilot = Autopilot { + autopilot: AutopilotConfig { + receiver: ica.to_string(), + stakeibc: crate::helpers::Stakeibc { + action: "LiquidStake".to_string(), + stride_address: ica, + }, + }, + }; + + let autopilot_str = to_json_string(&autopilot)?; + + Ok(to_json_binary(&autopilot_str)?) + } + } +} + +fn _query_deposit_address(deps: Deps, env: Env) -> Result, StdError> { + let key = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); + + // here we cover three cases: + INTERCHAIN_ACCOUNTS + .may_load(deps.storage, key) + .map(|entry| entry.flatten().map(|x| x.0)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: sudo: received sudo msg: {msg:?}").as_str()); + + match msg { + // For handling successful (non-error) acknowledgements. + SudoMsg::Response { request, data } => sudo_response(deps, request, data), + + // For handling error acknowledgements. + SudoMsg::Error { request, details } => sudo_error(deps, request, details), + + // For handling error timeouts. + SudoMsg::Timeout { request } => sudo_timeout(deps, env, request), + + // For handling successful registering of ICA + SudoMsg::OpenAck { + port_id, + channel_id, + counterparty_channel_id, + counterparty_version, + } => sudo_open_ack( + deps, + env, + port_id, + channel_id, + counterparty_channel_id, + counterparty_version, + ), + _ => Ok(Response::default()), + } +} + +// handler +fn sudo_open_ack( + deps: DepsMut, + _env: Env, + port_id: String, + _channel_id: String, + _counterparty_channel_id: String, + counterparty_version: String, +) -> StdResult { + // The version variable contains a JSON value with multiple fields, + // including the generated account address. + let parsed_version: Result = + serde_json_wasm::from_str(counterparty_version.as_str()); + + // get the parsed OpenAckVersion or return an error if we fail + let Ok(parsed_version) = parsed_version else { + return Err(StdError::generic_err("Can't parse counterparty_version")) + }; + + // Update the storage record associated with the interchain account. + INTERCHAIN_ACCOUNTS.save( + deps.storage, + port_id, + &Some(( + parsed_version.clone().address, + parsed_version.controller_connection_id, + )), + )?; + CONTRACT_STATE.save(deps.storage, &ContractState::IcaCreated)?; + + Ok(Response::default().add_attribute("method", "sudo_open_ack")) +} + +fn sudo_response(deps: DepsMut, request: RequestPacket, data: Binary) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: sudo_response: sudo received: {request:?} {data:?}",).as_str()); + + // either of these errors will close the channel + request + .sequence + .ok_or_else(|| StdError::generic_err("sequence not found"))?; + + request + .source_channel + .ok_or_else(|| StdError::generic_err("channel_id not found"))?; + + Ok(Response::default().add_attribute("method", "sudo_response")) +} + +fn sudo_timeout(deps: DepsMut, _env: Env, request: RequestPacket) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: sudo timeout request: {request:?}").as_str()); + + // revert the state to Instantiated to force re-creation of ICA + CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; + + // returning Ok as this is anticipated. channel is already closed. + Ok(Response::default()) +} + +fn sudo_error(deps: DepsMut, request: RequestPacket, details: String) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: sudo error: {details}").as_str()); + deps.api + .debug(format!("WASMDEBUG: request packet: {request:?}").as_str()); + + // either of these errors will close the channel + request + .sequence + .ok_or_else(|| StdError::generic_err("sequence not found"))?; + + request + .source_channel + .ok_or_else(|| StdError::generic_err("channel_id not found"))?; + + Ok(Response::default().add_attribute("method", "sudo_error")) +} + +// prepare_sudo_payload is called from reply handler +// The method is used to extract sequence id and channel from SubmitTxResponse to +// process sudo payload defined in msg_with_sudo_callback later in Sudo handler. +// Such flow msg_with_sudo_callback() -> reply() -> prepare_sudo_payload() -> sudo() +// allows you "attach" some payload to your SubmitTx message +// and process this payload when an acknowledgement for the SubmitTx message +// is received in Sudo handler +fn prepare_sudo_payload(mut deps: DepsMut, _env: Env, msg: Reply) -> StdResult { + let payload = read_reply_payload(deps.storage)?; + let resp: MsgSubmitTxResponse = serde_json_wasm::from_slice( + msg.result + .into_result() + .map_err(StdError::generic_err)? + .data + .ok_or_else(|| StdError::generic_err("no result"))? + .as_slice(), + ) + .map_err(|e| StdError::generic_err(format!("failed to parse response: {e:?}")))?; + deps.api + .debug(format!("WASMDEBUG: reply msg: {resp:?}").as_str()); + let seq_id = resp.sequence_id; + let channel_id = resp.channel; + save_sudo_payload(deps.branch().storage, channel_id, seq_id, payload)?; + Ok(Response::new()) +} + +fn get_ica( + deps: Deps, + env: &Env, + interchain_account_id: &str, +) -> Result<(String, String), StdError> { + let key = get_port_id(env.contract.address.as_str(), interchain_account_id); + + INTERCHAIN_ACCOUNTS + .load(deps.storage, key)? + .ok_or_else(|| StdError::generic_err("Interchain account is not created yet")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: reply msg: {msg:?}").as_str()); + match msg.id { + SUDO_PAYLOAD_REPLY_ID => prepare_sudo_payload(deps, env, msg), + _ => Err(StdError::generic_err(format!( + "unsupported reply message id {}", + msg.id + ))), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult { + deps.api.debug("WASMDEBUG: migrate"); + + match msg { + MigrateMsg::UpdateConfig { + clock_addr, + next_contract, + remote_chain_info, + } => { + let mut resp = Response::default().add_attribute("method", "update_config"); + + if let Some(addr) = clock_addr { + let addr = deps.api.addr_validate(&addr)?; + CLOCK_ADDRESS.save(deps.storage, &addr)?; + resp = resp.add_attribute("clock_addr", addr.to_string()); + } + + if let Some(addr) = next_contract { + let addr = deps.api.addr_validate(&addr)?; + resp = resp.add_attribute("next_contract", addr.to_string()); + NEXT_CONTRACT.save(deps.storage, &addr)?; + } + + if let Some(rci) = remote_chain_info { + let validated_rci = rci.validate()?; + REMOTE_CHAIN_INFO.save(deps.storage, &validated_rci)?; + resp = resp.add_attributes(validated_rci.get_response_attributes()); + } + + Ok(resp) + } + MigrateMsg::UpdateCodeId { data: _ } => { + // This is a migrate message to update code id, + // Data is optional base64 that we can parse to any data we would like in the future + // let data: SomeStruct = from_binary(&data)?; + Ok(Response::default()) + } + } +} diff --git a/contracts/stride-liquid-staker/src/error.rs b/contracts/stride-liquid-staker/src/error.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/contracts/stride-liquid-staker/src/error.rs @@ -0,0 +1 @@ + diff --git a/contracts/stride-liquid-staker/src/helpers.rs b/contracts/stride-liquid-staker/src/helpers.rs new file mode 100644 index 00000000..829f792d --- /dev/null +++ b/contracts/stride-liquid-staker/src/helpers.rs @@ -0,0 +1,20 @@ +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub struct AutopilotConfig { + pub receiver: String, + pub stakeibc: Stakeibc, +} + +#[cw_serde] +pub struct Autopilot { + pub autopilot: AutopilotConfig, +} + +#[cw_serde] +pub struct Stakeibc { + pub action: String, + pub stride_address: String, + // pub ibc_receiver: String, + // pub transfer_channel: String, +} diff --git a/contracts/stride-liquid-staker/src/lib.rs b/contracts/stride-liquid-staker/src/lib.rs new file mode 100644 index 00000000..8eaeae41 --- /dev/null +++ b/contracts/stride-liquid-staker/src/lib.rs @@ -0,0 +1,13 @@ +#![warn(clippy::unwrap_used, clippy::expect_used)] + +extern crate core; + +pub mod contract; +pub mod error; +pub mod helpers; +pub mod msg; +pub mod state; + +#[allow(clippy::unwrap_used)] +#[cfg(test)] +mod suite_test; diff --git a/contracts/stride-liquid-staker/src/msg.rs b/contracts/stride-liquid-staker/src/msg.rs new file mode 100644 index 00000000..5e78483f --- /dev/null +++ b/contracts/stride-liquid-staker/src/msg.rs @@ -0,0 +1,141 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{to_json_binary, Addr, Binary, StdError, Uint128, Uint64, WasmMsg}; +use covenant_macros::{ + clocked, covenant_clock_address, covenant_deposit_address, covenant_ica_address, + covenant_remote_chain, +}; +use covenant_utils::neutron_ica::RemoteChainInfo; +use neutron_sdk::bindings::msg::IbcFee; + +#[cw_serde] +pub struct InstantiateMsg { + /// Address for the clock. This contract verifies + /// that only the clock can execute Ticks + pub clock_address: String, + /// IBC transfer channel on Stride for Neutron + /// This is used to IBC transfer stuatom on Stride + /// to the LP contract + pub stride_neutron_ibc_transfer_channel_id: String, + /// IBC connection ID on Neutron for Stride + /// We make an Interchain Account over this connection + pub neutron_stride_ibc_connection_id: String, + /// Address of the next contract to query for the deposit address + pub next_contract: String, + /// The liquid staked denom (e.g., stuatom). This is + /// required because we only allow transfers of this denom + /// out of the LSer + pub ls_denom: String, + /// Neutron requires fees to be set to refund relayers for + /// submission of ack and timeout messages. + /// recv_fee and ack_fee paid in untrn from this contract + pub ibc_fee: IbcFee, + /// Time in seconds for ICA SubmitTX messages from Neutron + /// Note that ICA uses ordered channels, a timeout implies + /// channel closed. We can reopen the channel by reregistering + /// the ICA with the same port id and connection id + pub ica_timeout: Uint64, + /// Timeout in seconds. This is used to craft a timeout timestamp + /// that will be attached to the IBC transfer message from the ICA + /// on the host chain (Stride) to its destination. Typically + /// this timeout should be greater than the ICA timeout, otherwise + /// if the ICA times out, the destination chain receiving the funds + /// will also receive the IBC packet with an expired timestamp. + pub ibc_transfer_timeout: Uint64, +} + +#[cw_serde] +pub struct PresetStrideLsFields { + pub code_id: u64, + pub label: String, + pub ls_denom: String, + pub stride_neutron_ibc_transfer_channel_id: String, + pub neutron_stride_ibc_connection_id: String, + pub ica_timeout: Uint64, + pub ibc_transfer_timeout: Uint64, + pub ibc_fee: IbcFee, +} + +impl PresetStrideLsFields { + pub fn to_instantiate_msg( + &self, + clock_address: String, + next_contract: String, + ) -> InstantiateMsg { + InstantiateMsg { + clock_address, + stride_neutron_ibc_transfer_channel_id: self + .stride_neutron_ibc_transfer_channel_id + .to_string(), + neutron_stride_ibc_connection_id: self.neutron_stride_ibc_connection_id.to_string(), + next_contract, + ls_denom: self.ls_denom.to_string(), + ibc_fee: self.ibc_fee.clone(), + ica_timeout: self.ica_timeout, + ibc_transfer_timeout: self.ibc_transfer_timeout, + } + } + + pub fn to_instantiate2_msg( + &self, + admin_addr: String, + salt: Binary, + clock_address: String, + next_contract: String, + ) -> Result { + let instantiate_msg = self.to_instantiate_msg(clock_address, next_contract); + Ok(WasmMsg::Instantiate2 { + admin: Some(admin_addr), + code_id: self.code_id, + label: self.label.to_string(), + msg: to_json_binary(&instantiate_msg)?, + funds: vec![], + salt, + }) + } +} + +#[clocked] +#[cw_serde] +pub enum ExecuteMsg { + /// The transfer message allows anybody to permissionlessly + /// transfer a specified amount of tokens of the preset ls_denom + /// from the ICA of the host chain to the preset lp_address + Transfer { amount: Uint128 }, +} + +#[covenant_clock_address] +#[covenant_remote_chain] +#[covenant_deposit_address] +#[covenant_ica_address] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(ContractState)] + ContractState {}, + #[returns(String)] + NextMemo {}, +} + +#[cw_serde] +pub enum MigrateMsg { + UpdateConfig { + clock_addr: Option, + // stride_neutron_ibc_transfer_channel_id: Option, + next_contract: Option, + // neutron_stride_ibc_connection_id: Option, + // ls_denom: Option, + // ibc_fee: Option, + // ibc_transfer_timeout: Option, + // ica_timeout: Option, + remote_chain_info: Option, + }, + UpdateCodeId { + data: Option, + }, +} + +#[cw_serde] +pub enum ContractState { + Instantiated, + IcaCreated, +} diff --git a/contracts/stride-liquid-staker/src/state.rs b/contracts/stride-liquid-staker/src/state.rs new file mode 100644 index 00000000..2a866469 --- /dev/null +++ b/contracts/stride-liquid-staker/src/state.rs @@ -0,0 +1,77 @@ +use cosmwasm_std::{from_json, to_json_vec, Addr, Binary, Order, StdResult, Storage, Uint128}; +use covenant_utils::neutron_ica::{RemoteChainInfo, SudoPayload}; +use cw_storage_plus::{Item, Map}; + +use crate::msg::ContractState; + +/// tracks the current state of state machine +pub const CONTRACT_STATE: Item = Item::new("contract_state"); + +/// clock module address to verify the sender of incoming ticks +pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); +/// next contract address to forward the liquid staked funds to +pub const NEXT_CONTRACT: Item = Item::new("next_contract"); + +pub const TRANSFER_AMOUNT: Item = Item::new("transfer_amount"); + +/// information needed for an ibc transfer to the remote chain +pub const REMOTE_CHAIN_INFO: Item = Item::new("r_c_info"); + +/// interchain accounts storage in form of (port_id) -> (address, controller_connection_id) +pub const INTERCHAIN_ACCOUNTS: Map> = + Map::new("interchain_accounts"); + +/// interchain transaction responses - ack/err/timeout state to query later +// pub const ACKNOWLEDGEMENT_RESULTS: Map<(String, u64), AcknowledgementResult> = +// Map::new("acknowledgement_results"); +pub const REPLY_ID_STORAGE: Item> = Item::new("reply_queue_id"); +pub const SUDO_PAYLOAD: Map<(String, u64), Vec> = Map::new("sudo_payload"); +pub const ERRORS_QUEUE: Map = Map::new("errors_queue"); + +pub fn save_reply_payload(store: &mut dyn Storage, payload: SudoPayload) -> StdResult<()> { + REPLY_ID_STORAGE.save(store, &to_json_vec(&payload)?) +} + +pub fn read_reply_payload(store: &mut dyn Storage) -> StdResult { + let data = REPLY_ID_STORAGE.load(store)?; + from_json(Binary(data)) +} + +pub fn add_error_to_queue(store: &mut dyn Storage, error_msg: String) -> Option<()> { + let result = ERRORS_QUEUE + .keys(store, None, None, Order::Descending) + .next() + .and_then(|data| data.ok()) + .map(|c| c + 1) + .or(Some(0)); + + result.and_then(|idx| ERRORS_QUEUE.save(store, idx, &error_msg).ok()) +} + +pub fn read_errors_from_queue(store: &dyn Storage) -> StdResult, String)>> { + ERRORS_QUEUE + .range_raw(store, None, None, Order::Ascending) + .collect() +} + +pub fn read_sudo_payload( + store: &mut dyn Storage, + channel_id: String, + seq_id: u64, +) -> StdResult { + let data = SUDO_PAYLOAD.load(store, (channel_id, seq_id))?; + from_json(Binary(data)) +} + +pub fn save_sudo_payload( + store: &mut dyn Storage, + channel_id: String, + seq_id: u64, + payload: SudoPayload, +) -> StdResult<()> { + SUDO_PAYLOAD.save(store, (channel_id, seq_id), &to_json_vec(&payload)?) +} + +pub fn clear_sudo_payload(store: &mut dyn Storage, channel_id: String, seq_id: u64) { + SUDO_PAYLOAD.remove(store, (channel_id, seq_id)) +} diff --git a/contracts/stride-liquid-staker/src/suite_test/mod.rs b/contracts/stride-liquid-staker/src/suite_test/mod.rs new file mode 100644 index 00000000..7b881830 --- /dev/null +++ b/contracts/stride-liquid-staker/src/suite_test/mod.rs @@ -0,0 +1,2 @@ +mod suite; +mod tests; diff --git a/contracts/stride-liquid-staker/src/suite_test/suite.rs b/contracts/stride-liquid-staker/src/suite_test/suite.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/contracts/stride-liquid-staker/src/suite_test/suite.rs @@ -0,0 +1 @@ + diff --git a/contracts/stride-liquid-staker/src/suite_test/tests.rs b/contracts/stride-liquid-staker/src/suite_test/tests.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/contracts/stride-liquid-staker/src/suite_test/tests.rs @@ -0,0 +1 @@ + diff --git a/contracts/swap-covenant/src/contract.rs b/contracts/swap-covenant/src/contract.rs index 2704c3df..41f3effc 100644 --- a/contracts/swap-covenant/src/contract.rs +++ b/contracts/swap-covenant/src/contract.rs @@ -76,11 +76,11 @@ pub fn instantiate( let party_b_forwarder_salt = generate_contract_salt(PARTY_B_FORWARDER_SALT); let splitter_salt = generate_contract_salt(SPLITTER_SALT); - let party_a_router_code = match msg.clone().party_a_config { + let party_a_router_code = match msg.party_a_config { CovenantPartyConfig::Interchain(_) => msg.contract_codes.interchain_router_code, CovenantPartyConfig::Native(_) => msg.contract_codes.native_router_code, }; - let party_b_router_code = match msg.clone().party_b_config { + let party_b_router_code = match msg.party_b_config { CovenantPartyConfig::Interchain(_) => msg.contract_codes.interchain_router_code, CovenantPartyConfig::Native(_) => msg.contract_codes.native_router_code, }; diff --git a/contracts/swap-holder/src/suite_tests/suite.rs b/contracts/swap-holder/src/suite_tests/suite.rs index fcdd92fc..011b4b47 100644 --- a/contracts/swap-holder/src/suite_tests/suite.rs +++ b/contracts/swap-holder/src/suite_tests/suite.rs @@ -97,7 +97,7 @@ impl SuiteBuilder { .unwrap(); self.instantiate.clock_address = clock_address.to_string(); - println!("clock address: {:?}", clock_address); + println!("clock address: {clock_address:?}"); let mock_deposit = app .instantiate_contract( diff --git a/contracts/two-party-pol-covenant/src/contract.rs b/contracts/two-party-pol-covenant/src/contract.rs index b411b113..ff723ce3 100644 --- a/contracts/two-party-pol-covenant/src/contract.rs +++ b/contracts/two-party-pol-covenant/src/contract.rs @@ -84,12 +84,12 @@ pub fn instantiate( &clock_salt, )?; - let party_a_router_code = match msg.clone().party_a_config { + let party_a_router_code = match msg.party_a_config { CovenantPartyConfig::Native(_) => msg.contract_codes.native_router_code, CovenantPartyConfig::Interchain(_) => msg.contract_codes.interchain_router_code, }; - let party_b_router_code = match msg.clone().party_b_config { + let party_b_router_code = match msg.party_b_config { CovenantPartyConfig::Native(_) => msg.contract_codes.native_router_code, CovenantPartyConfig::Interchain(_) => msg.contract_codes.interchain_router_code, }; diff --git a/contracts/two-party-pol-holder/src/suite_tests/suite.rs b/contracts/two-party-pol-holder/src/suite_tests/suite.rs index 6d161f7e..98027712 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/suite.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/suite.rs @@ -243,7 +243,7 @@ impl SuiteBuilder { Some(ADMIN.to_string()), ) .unwrap(); - println!("holder address: {:?}", holder); + println!("holder address: {holder:?}"); Suite { app, diff --git a/interchaintest/single-party-pol/README.md b/interchaintest/single-party-pol/README.md new file mode 100644 index 00000000..972b3d82 --- /dev/null +++ b/interchaintest/single-party-pol/README.md @@ -0,0 +1 @@ +# single party pol e2e tests diff --git a/interchaintest/single-party-pol/chains.yaml b/interchaintest/single-party-pol/chains.yaml new file mode 100644 index 00000000..06ba69b9 --- /dev/null +++ b/interchaintest/single-party-pol/chains.yaml @@ -0,0 +1,59 @@ +## Set the environment variable: IBCTEST_CONFIGURED_CHAINS to a path +## to use custom versions of this file + +gaia: + name: gaia + type: cosmos + bin: gaiad + bech32-prefix: cosmos + denom: uatom + gas-prices: 0.01uatom + gas-adjustment: 1.3 + trusting-period: 504h + images: + - repository: ghcr.io/strangelove-ventures/heighliner/gaia + uid-gid: 1025:1025 + no-host-mount: false + +neutron: + name: neutron + type: cosmos + bin: neutrond + bech32-prefix: neutron + denom: untrn + gas-prices: 0.01untrn + gas-adjustment: 1.3 + trusting-period: 336h + images: + - repository: ghcr.io/strangelove-ventures/heighliner/neutron + uid-gid: 1025:1025 + no-host-mount: false + +persistence: + name: persistence + type: cosmos + bin: persistenceCore + bech32-prefix: persistence + denom: uxprt + gas-prices: 0.01uxprt + gas-adjustment: 1.3 + coin-type: 750 + trusting-period: "504h" + images: + - repository: ghcr.io/strangelove-ventures/heighliner/persistence + uid-gid: 1025:1025 + no-host-mount: false + +stride: + name: stride + type: cosmos + bin: strided + bech32-prefix: stride + denom: ustrd + gas-prices: 0.01ustrd + gas-adjustment: 1.3 + trusting-period: "336h" + images: + - repository: ghcr.io/strangelove-ventures/heighliner/stride + uid-gid: 1025:1025 + no-host-mount: false diff --git a/interchaintest/single-party-pol/single_party_pol_test.go b/interchaintest/single-party-pol/single_party_pol_test.go new file mode 100644 index 00000000..35fa8f84 --- /dev/null +++ b/interchaintest/single-party-pol/single_party_pol_test.go @@ -0,0 +1,933 @@ +package covenant_single_party_pol + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/crypto/keyring" + ibctest "github.com/strangelove-ventures/interchaintest/v4" + "github.com/strangelove-ventures/interchaintest/v4/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v4/ibc" + "github.com/strangelove-ventures/interchaintest/v4/relayer" + "github.com/strangelove-ventures/interchaintest/v4/testreporter" + "github.com/strangelove-ventures/interchaintest/v4/testutil" + "github.com/stretchr/testify/require" + utils "github.com/timewave-computer/covenants/interchaintest/utils" + "go.uber.org/zap" + "go.uber.org/zap/zaptest" +) + +const gaiaNeutronICSPath = "gn-ics-path" +const gaiaNeutronIBCPath = "gn-ibc-path" +const gaiaStrideIBCPath = "go-ibc-path" +const neutronStrideIBCPath = "no-ibc-path" +const nativeAtomDenom = "uatom" +const nativeStatomDenom = "stuatom" +const nativeNtrnDenom = "untrn" + +var covenantAddress string +var clockAddress string +var liquidPoolerAddress string +var partyDepositAddress string +var holderAddress string +var liquidStakerAddress string +var lsForwarderAddress string +var remoteChainSplitterAddress string +var liquidPoolerForwarderAddress string +var strideIcaAddress string +var lsForwarderIcaAddress, liquidPoolerForwarderIcaAddress string + +var neutronAtomIbcDenom, neutronStatomIbcDenom, strideAtomIbcDenom string +var atomNeutronICSConnectionId, neutronAtomICSConnectionId string +var neutronStrideIBCConnId, strideNeutronIBCConnId string +var atomNeutronIBCConnId, neutronAtomIBCConnId string +var atomStrideIBCConnId, strideAtomIBCConnId string +var gaiaStrideIBCConnId, strideGaiaIBCConnId string +var tokenAddress string +var whitelistAddress string +var factoryAddress string +var coinRegistryAddress string +var stableswapAddress string +var liquidityTokenAddress string + +// PARTY_A +const atomContributionAmount uint64 = 5_000_000_000 // in uatom + +// sets up and tests a single party pol by hub +func TestSinglePartyPol(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode") + } + + os.Setenv("IBCTEST_CONFIGURED_CHAINS", "./chains.yaml") + + ctx := context.Background() + + // Modify the the timeout_commit in the config.toml node files + // to reduce the block commit times. This speeds up the tests + // by about 35% + configFileOverrides := make(map[string]any) + configTomlOverrides := make(testutil.Toml) + consensus := make(testutil.Toml) + consensus["timeout_commit"] = "1s" + configTomlOverrides["consensus"] = consensus + configFileOverrides["config/config.toml"] = configTomlOverrides + + // Chain Factory + cf := ibctest.NewBuiltinChainFactory(zaptest.NewLogger(t, zaptest.Level(zap.WarnLevel)), []*ibctest.ChainSpec{ + {Name: "gaia", Version: "v9.1.0", ChainConfig: ibc.ChainConfig{ + GasAdjustment: 1.3, + GasPrices: "0.0atom", + ModifyGenesis: utils.SetupGaiaGenesis(utils.GetDefaultInterchainGenesisMessages()), + ConfigFileOverrides: configFileOverrides, + }}, + { + ChainConfig: ibc.ChainConfig{ + Type: "cosmos", + Name: "neutron", + ChainID: "neutron-2", + Images: []ibc.DockerImage{ + { + Repository: "ghcr.io/strangelove-ventures/heighliner/neutron", + Version: "v2.0.0", + UidGid: "1025:1025", + }, + }, + Bin: "neutrond", + Bech32Prefix: "neutron", + Denom: nativeNtrnDenom, + GasPrices: "0.0untrn,0.0uatom", + GasAdjustment: 1.3, + TrustingPeriod: "1197504s", + NoHostMount: false, + ModifyGenesis: utils.SetupNeutronGenesis( + "0.05", + []string{nativeNtrnDenom}, + []string{nativeAtomDenom}, + utils.GetDefaultNeutronInterchainGenesisMessages(), + ), + ConfigFileOverrides: configFileOverrides, + }, + }, + { + ChainConfig: ibc.ChainConfig{ + Type: "cosmos", + Name: "stride", + ChainID: "stride-3", + Images: []ibc.DockerImage{ + { + Repository: "stride", + Version: "non-ics", + UidGid: "1025:1025", + }, + }, + Bin: "strided", + Bech32Prefix: "stride", + Denom: "ustrd", + ModifyGenesis: utils.SetupStrideGenesis([]string{ + "/cosmos.bank.v1beta1.MsgSend", + "/cosmos.bank.v1beta1.MsgMultiSend", + "/cosmos.staking.v1beta1.MsgDelegate", + "/cosmos.staking.v1beta1.MsgUndelegate", + "/cosmos.staking.v1beta1.MsgBeginRedelegate", + "/cosmos.staking.v1beta1.MsgRedeemTokensforShares", + "/cosmos.staking.v1beta1.MsgTokenizeShares", + "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward", + "/cosmos.distribution.v1beta1.MsgSetWithdrawAddress", + "/ibc.applications.transfer.v1.MsgTransfer", + }), + GasPrices: "0.0ustrd", + GasAdjustment: 1.3, + TrustingPeriod: "336h", + NoHostMount: false, + ConfigFileOverrides: configFileOverrides, + }, + }, + }) + + chains, err := cf.Chains(t.Name()) + require.NoError(t, err) + + // We have three chains + atom, neutron, stride := chains[0], chains[1], chains[2] + cosmosAtom, cosmosNeutron, cosmosStride := atom.(*cosmos.CosmosChain), neutron.(*cosmos.CosmosChain), stride.(*cosmos.CosmosChain) + + // Relayer Factory + client, network := ibctest.DockerSetup(t) + r := ibctest.NewBuiltinRelayerFactory( + ibc.CosmosRly, + zaptest.NewLogger(t, zaptest.Level(zap.InfoLevel)), + relayer.CustomDockerImage("ghcr.io/cosmos/relayer", "v2.4.0", "1000:1000"), + relayer.RelayerOptionExtraStartFlags{Flags: []string{"-p", "events", "-b", "100", "-d", "--log-format", "console"}}, + ).Build(t, client, network) + + // Prep Interchain + ic := ibctest.NewInterchain(). + AddChain(cosmosAtom). + AddChain(cosmosNeutron). + AddChain(cosmosStride). + AddRelayer(r, "relayer"). + AddProviderConsumerLink(ibctest.ProviderConsumerLink{ + Provider: cosmosAtom, + Consumer: cosmosNeutron, + Relayer: r, + Path: gaiaNeutronICSPath, + }). + AddLink(ibctest.InterchainLink{ + Chain1: cosmosAtom, + Chain2: cosmosNeutron, + Relayer: r, + Path: gaiaNeutronIBCPath, + }). + AddLink(ibctest.InterchainLink{ + Chain1: cosmosNeutron, + Chain2: cosmosStride, + Relayer: r, + Path: neutronStrideIBCPath, + }). + AddLink(ibctest.InterchainLink{ + Chain1: cosmosAtom, + Chain2: cosmosStride, + Relayer: r, + Path: gaiaStrideIBCPath, + }) + + // Log location + f, err := ibctest.CreateLogFile(fmt.Sprintf("%d.json", time.Now().Unix())) + require.NoError(t, err) + // Reporter/logs + rep := testreporter.NewReporter(f) + eRep := rep.RelayerExecReporter(t) + + // Build interchain + require.NoError( + t, + ic.Build(ctx, eRep, ibctest.InterchainBuildOptions{ + TestName: t.Name(), + Client: client, + NetworkID: network, + BlockDatabaseFile: ibctest.DefaultBlockDatabaseFilepath(), + SkipPathCreation: true, + }), + "failed to build interchain") + + testCtx := &utils.TestContext{ + Neutron: cosmosNeutron, + Hub: cosmosAtom, + Stride: cosmosStride, + StrideClients: []*ibc.ClientOutput{}, + GaiaClients: []*ibc.ClientOutput{}, + NeutronClients: []*ibc.ClientOutput{}, + StrideConnections: []*ibc.ConnectionOutput{}, + GaiaConnections: []*ibc.ConnectionOutput{}, + NeutronConnections: []*ibc.ConnectionOutput{}, + NeutronTransferChannelIds: make(map[string]string), + GaiaTransferChannelIds: make(map[string]string), + StrideTransferChannelIds: make(map[string]string), + GaiaIcsChannelIds: make(map[string]string), + NeutronIcsChannelIds: make(map[string]string), + T: t, + Ctx: ctx, + } + + testCtx.SkipBlocksStride(5) + + t.Run("generate IBC paths", func(t *testing.T) { + utils.GeneratePath(t, ctx, r, eRep, cosmosAtom.Config().ChainID, cosmosNeutron.Config().ChainID, gaiaNeutronIBCPath) + utils.GeneratePath(t, ctx, r, eRep, cosmosAtom.Config().ChainID, cosmosStride.Config().ChainID, gaiaStrideIBCPath) + utils.GeneratePath(t, ctx, r, eRep, cosmosNeutron.Config().ChainID, cosmosStride.Config().ChainID, neutronStrideIBCPath) + utils.GeneratePath(t, ctx, r, eRep, cosmosNeutron.Config().ChainID, cosmosAtom.Config().ChainID, gaiaNeutronICSPath) + }) + + t.Run("setup neutron-gaia ICS", func(t *testing.T) { + utils.GenerateClient(t, ctx, testCtx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) + neutronClients := testCtx.GetChainClients(cosmosNeutron.Config().Name) + atomClients := testCtx.GetChainClients(cosmosAtom.Config().Name) + + err = r.UpdatePath(ctx, eRep, gaiaNeutronICSPath, ibc.PathUpdateOptions{ + SrcClientID: &neutronClients[0].ClientID, + DstClientID: &atomClients[0].ClientID, + }) + require.NoError(t, err) + + atomNeutronICSConnectionId, neutronAtomICSConnectionId = utils.GenerateConnections(t, ctx, testCtx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) + + utils.GenerateICSChannel(t, ctx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) + + utils.CreateValidator(t, ctx, r, eRep, atom, neutron) + testCtx.SkipBlocksStride(2) + }) + + t.Run("setup IBC interchain clients, connections, and links", func(t *testing.T) { + utils.GenerateClient(t, ctx, testCtx, r, eRep, neutronStrideIBCPath, cosmosNeutron, cosmosStride) + neutronStrideIBCConnId, strideNeutronIBCConnId = utils.GenerateConnections(t, ctx, testCtx, r, eRep, neutronStrideIBCPath, cosmosNeutron, cosmosStride) + utils.LinkPath(t, ctx, r, eRep, cosmosNeutron, cosmosStride, neutronStrideIBCPath) + + utils.GenerateClient(t, ctx, testCtx, r, eRep, gaiaStrideIBCPath, cosmosAtom, cosmosStride) + gaiaStrideIBCConnId, strideGaiaIBCConnId = utils.GenerateConnections(t, ctx, testCtx, r, eRep, gaiaStrideIBCPath, cosmosAtom, cosmosStride) + utils.LinkPath(t, ctx, r, eRep, cosmosAtom, cosmosStride, gaiaStrideIBCPath) + + utils.GenerateClient(t, ctx, testCtx, r, eRep, gaiaNeutronIBCPath, cosmosAtom, cosmosNeutron) + atomNeutronIBCConnId, neutronAtomIBCConnId = utils.GenerateConnections(t, ctx, testCtx, r, eRep, gaiaNeutronIBCPath, cosmosAtom, cosmosNeutron) + utils.LinkPath(t, ctx, r, eRep, cosmosAtom, cosmosNeutron, gaiaNeutronIBCPath) + }) + + // Start the relayer and clean it up when the test ends. + err = r.StartRelayer(ctx, eRep, gaiaNeutronICSPath, gaiaNeutronIBCPath, gaiaStrideIBCPath, neutronStrideIBCPath) + require.NoError(t, err, "failed to start relayer with given paths") + t.Cleanup(func() { + err = r.StopRelayer(ctx, eRep) + if err != nil { + t.Logf("failed to stop relayer: %s", err) + } + }) + testCtx.SkipBlocksStride(2) + + // Once the VSC packet has been relayed, x/bank transfers are + // enabled on Neutron and we can fund its account. + // The funds for this are sent from a "faucet" account created + // by interchaintest in the genesis file. + users := ibctest.GetAndFundTestUsers(t, ctx, "default", int64(500_000_000_000), atom, neutron, stride) + gaiaUser, neutronUser, strideUser := users[0], users[1], users[2] + _, _, _ = gaiaUser, neutronUser, strideUser + + strideAdminMnemonic := "tone cause tribe this switch near host damage idle fragile antique tail soda alien depth write wool they rapid unfold body scan pledge soft" + strideAdmin, _ := ibctest.GetAndFundTestUserWithMnemonic(ctx, "default", strideAdminMnemonic, (100_000_000), cosmosStride) + + cosmosStride.SendFunds(ctx, strideUser.KeyName, ibc.WalletAmount{ + Address: strideAdmin.Bech32Address(stride.Config().Bech32Prefix), + Denom: "ustrd", + Amount: 10000000, + }) + + testCtx.SkipBlocksStride(5) + + t.Run("determine ibc channels", func(t *testing.T) { + neutronChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosNeutron.Config().ChainID) + gaiaChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosAtom.Config().ChainID) + strideChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosStride.Config().ChainID) + + // Find all pairwise channels + utils.GetPairwiseTransferChannelIds(testCtx, strideChannelInfo, neutronChannelInfo, strideNeutronIBCConnId, neutronStrideIBCConnId, stride.Config().Name, neutron.Config().Name) + utils.GetPairwiseTransferChannelIds(testCtx, strideChannelInfo, gaiaChannelInfo, strideGaiaIBCConnId, gaiaStrideIBCConnId, stride.Config().Name, cosmosAtom.Config().Name) + utils.GetPairwiseTransferChannelIds(testCtx, gaiaChannelInfo, neutronChannelInfo, atomNeutronIBCConnId, neutronAtomIBCConnId, cosmosAtom.Config().Name, neutron.Config().Name) + utils.GetPairwiseCCVChannelIds(testCtx, gaiaChannelInfo, neutronChannelInfo, atomNeutronICSConnectionId, neutronAtomICSConnectionId, cosmosAtom.Config().Name, cosmosNeutron.Config().Name) + }) + + t.Run("determine ibc denoms", func(t *testing.T) { + // We can determine the ibc denoms of: + // 1. ATOM on Neutron + neutronAtomIbcDenom = testCtx.GetIbcDenom( + testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name], + nativeAtomDenom, + ) + // 2. statom on neutron + neutronStatomIbcDenom = testCtx.GetIbcDenom( + testCtx.NeutronTransferChannelIds[cosmosStride.Config().Name], + nativeStatomDenom, + ) + // 3. atom on stride + strideAtomIbcDenom = testCtx.GetIbcDenom( + testCtx.StrideTransferChannelIds[cosmosAtom.Config().Name], + nativeAtomDenom, + ) + }) + + // Stride is a liquid staking platform. We need to register Gaia (ATOM) + // as a host zone in order to redeem stATOM in exchange for ATOM + // stATOM is stride's liquid staked ATOM vouchers. + t.Run("register stride host zone", func(t *testing.T) { + + cmd := []string{"strided", "tx", "stakeibc", "register-host-zone", + strideGaiaIBCConnId, + cosmosAtom.Config().Denom, + cosmosAtom.Config().Bech32Prefix, + strideAtomIbcDenom, + testCtx.StrideTransferChannelIds[cosmosAtom.Config().Name], + "1", + "--from", strideAdmin.KeyName, + "--gas", "auto", + "--gas-adjustment", `1.3`, + "--output", "json", + "--chain-id", cosmosStride.Config().ChainID, + "--node", cosmosStride.GetRPCAddress(), + "--home", cosmosStride.HomeDir(), + "--keyring-backend", keyring.BackendTest, + "-y", + } + + _, _, err = cosmosStride.Exec(ctx, cmd, nil) + require.NoError(t, err, "failed to register host zone on stride") + + testCtx.SkipBlocksStride(8) + }) + // Stride needs validators that it can stake ATOM with to issue us stATOM + t.Run("register gaia validators on stride", func(t *testing.T) { + + valcmd := []string{"gaiad", "query", "tendermint-validator-set", + "50", + "--chain-id", cosmosAtom.Config().ChainID, + "--node", cosmosAtom.GetRPCAddress(), + "--home", cosmosAtom.HomeDir(), + } + resp, _, err := cosmosAtom.Exec(ctx, valcmd, nil) + require.NoError(t, err, "Failed to query valset") + testCtx.SkipBlocksStride(2) + + var addresses []string + var votingPowers []string + + lines := strings.Split(string(resp), "\n") + + for _, line := range lines { + if strings.HasPrefix(line, "- address: ") { + address := strings.TrimPrefix(line, "- address: ") + addresses = append(addresses, address) + } else if strings.HasPrefix(line, " voting_power: ") { + votingPower := strings.TrimPrefix(line, " voting_power: ") + votingPowers = append(votingPowers, votingPower) + } + } + + // Create validators slice + var validators []Validator + + for i := 1; i <= len(addresses); i++ { + votingPowStr := strings.ReplaceAll(votingPowers[i-1], "\"", "") + valWeight, err := strconv.Atoi(votingPowStr) + require.NoError(t, err, "failed to parse voting power") + + validator := Validator{ + Name: fmt.Sprintf("val%d", i), + Address: addresses[i-1], + Weight: valWeight, + } + validators = append(validators, validator) + } + + // Create JSON object + data := map[string][]Validator{ + "validators": validators, + } + + // Convert to JSON + jsonData, err := json.Marshal(data) + require.NoError(t, err, "failed to marshall data") + + fullPath := filepath.Join(cosmosStride.HomeDir(), "vals.json") + bashCommand := "echo '" + string(jsonData) + "' > " + fullPath + fullPathCmd := []string{"/bin/sh", "-c", bashCommand} + + _, _, err = cosmosStride.Exec(ctx, fullPathCmd, nil) + require.NoError(t, err, "failed to create json with gaia LS validator set on stride") + testCtx.SkipBlocksStride(5) + + cmd := []string{"strided", "tx", "stakeibc", "add-validators", + cosmosAtom.Config().ChainID, + fullPath, + "--from", strideAdmin.KeyName, + "--gas", "auto", + "--gas-adjustment", `1.3`, + "--output", "json", + "--chain-id", cosmosStride.Config().ChainID, + "--node", cosmosStride.GetRPCAddress(), + "--home", cosmosStride.HomeDir(), + "--keyring-backend", keyring.BackendTest, + "-y", + } + + _, _, err = cosmosStride.Exec(ctx, cmd, nil) + require.NoError(t, err, "failed to register host zone on stride") + + testCtx.SkipBlocksStride(5) + + queryCmd := []string{"strided", "query", "stakeibc", + "show-validators", + cosmosAtom.Config().ChainID, + "--chain-id", cosmosStride.Config().ChainID, + "--node", cosmosStride.GetRPCAddress(), + "--home", cosmosStride.HomeDir(), + } + + _, _, err = cosmosStride.Exec(ctx, queryCmd, nil) + require.NoError(t, err, "failed to query host validators") + }) + + t.Run("two party pol covenant setup", func(t *testing.T) { + // Wasm code that we need to store on Neutron + const covenantContractPath = "wasms/covenant_single_party_pol.wasm" + const clockContractPath = "wasms/covenant_clock.wasm" + const interchainRouterContractPath = "wasms/covenant_interchain_router.wasm" + const nativeRouterContractPath = "wasms/covenant_native_router.wasm" + const ibcForwarderContractPath = "wasms/covenant_ibc_forwarder.wasm" + const holderContractPath = "wasms/covenant_single_party_pol_holder.wasm" + const liquidPoolerPath = "wasms/covenant_astroport_liquid_pooler.wasm" + const remoteChainSplitterPath = "wasms/covenant_native_splitter.wasm" + const liquidStakerContractPath = "wasms/covenant_stride_liquid_staker.wasm" + + // After storing on Neutron, we will receive a code id + // We parse all the subcontracts into uint64 + // The will be required when we instantiate the covenant. + var clockCodeId uint64 + var interchainRouterCodeId uint64 + var nativeRouterCodeId uint64 + var ibcForwarderCodeId uint64 + var holderCodeId uint64 + var lperCodeId uint64 + var liquidStakerCodeId uint64 + var covenantCodeId uint64 + var remoteChainSplitterCodeId uint64 + _, _, _, _, _, _, _, _, _ = clockCodeId, interchainRouterCodeId, nativeRouterCodeId, ibcForwarderCodeId, holderCodeId, lperCodeId, covenantCodeId, remoteChainSplitterCodeId, liquidStakerCodeId + + t.Run("deploy covenant contracts", func(t *testing.T) { + covenantCodeId = testCtx.StoreContract(cosmosNeutron, neutronUser, covenantContractPath) + + // store clock and get code id + clockCodeId = testCtx.StoreContract(cosmosNeutron, neutronUser, clockContractPath) + + // store routers and get code id + interchainRouterCodeId = testCtx.StoreContract(cosmosNeutron, neutronUser, interchainRouterContractPath) + nativeRouterCodeId = testCtx.StoreContract(cosmosNeutron, neutronUser, nativeRouterContractPath) + + // store forwarder and get code id + ibcForwarderCodeId = testCtx.StoreContract(cosmosNeutron, neutronUser, ibcForwarderContractPath) + + // store lper, get code + lperCodeId = testCtx.StoreContract(cosmosNeutron, neutronUser, liquidPoolerPath) + + // store holder and get code id + holderCodeId = testCtx.StoreContract(cosmosNeutron, neutronUser, holderContractPath) + + liquidStakerCodeId = testCtx.StoreContract(cosmosNeutron, neutronUser, liquidStakerContractPath) + // store remote chain splitter and get code id + remoteChainSplitterCodeId = testCtx.StoreContract(cosmosNeutron, neutronUser, remoteChainSplitterPath) + + testCtx.SkipBlocksStride(5) + }) + + t.Run("deploy astroport contracts", func(t *testing.T) { + stablePairCodeId := testCtx.StoreContract(cosmosNeutron, neutronUser, "wasms/astroport_pair_stable.wasm") + factoryCodeId := testCtx.StoreContract(cosmosNeutron, neutronUser, "wasms/astroport_factory.wasm") + whitelistCodeId := testCtx.StoreContract(cosmosNeutron, neutronUser, "wasms/astroport_whitelist.wasm") + tokenCodeId := testCtx.StoreContract(cosmosNeutron, neutronUser, "wasms/astroport_token.wasm") + coinRegistryCodeId := testCtx.StoreContract(cosmosNeutron, neutronUser, "wasms/astroport_native_coin_registry.wasm") + + t.Run("astroport token", func(t *testing.T) { + msg := NativeTokenInstantiateMsg{ + Name: "nativetoken", + Symbol: "ntk", + Decimals: 5, + InitialBalances: []Cw20Coin{}, + Mint: nil, + Marketing: nil, + } + str, _ := json.Marshal(msg) + + tokenAddress, err = cosmosNeutron.InstantiateContract( + ctx, neutronUser.KeyName, strconv.FormatUint(tokenCodeId, 10), string(str), true) + require.NoError(t, err, "Failed to instantiate nativetoken") + println("astroport token: ", tokenAddress) + }) + + t.Run("whitelist", func(t *testing.T) { + msg := WhitelistInstantiateMsg{ + Admins: []string{neutronUser.Bech32Address(neutron.Config().Bech32Prefix)}, + Mutable: false, + } + str, _ := json.Marshal(msg) + + whitelistAddress, err = cosmosNeutron.InstantiateContract( + ctx, neutronUser.KeyName, strconv.FormatUint(whitelistCodeId, 10), string(str), true) + require.NoError(t, err, "Failed to instantiate Whitelist") + println("astroport whitelist: ", whitelistAddress) + + }) + + t.Run("native coins registry", func(t *testing.T) { + msg := NativeCoinRegistryInstantiateMsg{ + Owner: neutronUser.Bech32Address(neutron.Config().Bech32Prefix), + } + str, _ := json.Marshal(msg) + + nativeCoinRegistryAddress, err := cosmosNeutron.InstantiateContract( + ctx, neutronUser.KeyName, strconv.FormatUint(coinRegistryCodeId, 10), string(str), true) + require.NoError(t, err, "Failed to instantiate NativeCoinRegistry") + coinRegistryAddress = nativeCoinRegistryAddress + println("astroport native coins registry: ", coinRegistryAddress) + }) + + t.Run("add coins to registry", func(t *testing.T) { + // Add ibc native tokens for statom and uatom to the native coin registry + // each of these tokens has a precision of 6 + addMessage := fmt.Sprintf( + `{"add":{"native_coins":[["%s",6],["%s",6]]}}`, + neutronAtomIbcDenom, + neutronStatomIbcDenom) + _, err = cosmosNeutron.ExecuteContract(ctx, neutronUser.KeyName, coinRegistryAddress, addMessage) + require.NoError(t, err, err) + testCtx.SkipBlocksStride(2) + }) + + t.Run("factory", func(t *testing.T) { + factoryAddress = testCtx.InstantiateAstroportFactory( + stablePairCodeId, tokenCodeId, whitelistCodeId, factoryCodeId, coinRegistryAddress, neutronUser) + println("astroport factory: ", factoryAddress) + testCtx.SkipBlocksStride(2) + }) + + t.Run("create pair on factory", func(t *testing.T) { + testCtx.CreateAstroportFactoryPairStride(3, neutronStatomIbcDenom, neutronAtomIbcDenom, factoryAddress, neutronUser, keyring.BackendTest) + }) + }) + + t.Run("fund stride user with atom to liquidstake", func(t *testing.T) { + + autopilotString := `{"autopilot":{"receiver":"` + strideUser.Bech32Address(stride.Config().Bech32Prefix) + `","stakeibc":{"stride_address":"` + strideUser.Bech32Address(stride.Config().Bech32Prefix) + `","action":"LiquidStake"}}}` + cmd := []string{cosmosAtom.Config().Bin, "tx", "ibc-transfer", "transfer", "transfer", + testCtx.GaiaTransferChannelIds[cosmosStride.Config().Name], autopilotString, + "100000000000uatom", + "--keyring-backend", keyring.BackendTest, + "--node", cosmosAtom.GetRPCAddress(), + "--from", gaiaUser.KeyName, + "--gas", "auto", + "--home", cosmosAtom.HomeDir(), + "--chain-id", cosmosAtom.Config().ChainID, + "-y", + } + _, _, err = cosmosAtom.Exec(ctx, cmd, nil) + require.NoError(t, err) + + testCtx.SkipBlocksStride(10) + + // ibc transfer statom on stride to neutron user + transferStAtomNeutron := ibc.WalletAmount{ + Address: neutronUser.Bech32Address(neutron.Config().Bech32Prefix), + Denom: nativeStatomDenom, + Amount: int64(100000000000), + } + _, err = cosmosStride.SendIBCTransfer(ctx, testCtx.StrideTransferChannelIds[cosmosNeutron.Config().Name], strideUser.KeyName, transferStAtomNeutron, ibc.TransferOptions{}) + require.NoError(t, err) + + testCtx.SkipBlocksStride(10) + }) + + t.Run("add liquidity to the atom-statom stableswap pool", func(t *testing.T) { + liquidityTokenAddress, stableswapAddress = testCtx.QueryAstroLpTokenAndStableswapAddress( + factoryAddress, neutronStatomIbcDenom, neutronAtomIbcDenom) + // set up the pool with 1:10 ratio of atom/statom + _, err := atom.SendIBCTransfer(ctx, + testCtx.GaiaTransferChannelIds[cosmosNeutron.Config().Name], + gaiaUser.KeyName, + ibc.WalletAmount{ + Address: neutronUser.Bech32Address(neutron.Config().Bech32Prefix), + Denom: cosmosAtom.Config().Denom, + Amount: int64(atomContributionAmount), + }, + ibc.TransferOptions{}) + require.NoError(t, err) + + testCtx.SkipBlocksStride(2) + + testCtx.ProvideAstroportLiquidity( + neutronAtomIbcDenom, neutronStatomIbcDenom, atomContributionAmount/2, atomContributionAmount/2, neutronUser, stableswapAddress) + + testCtx.SkipBlocksStride(2) + neutronUserLPTokenBal := testCtx.QueryLpTokenBalance(liquidityTokenAddress, neutronUser.Bech32Address(neutron.Config().Bech32Prefix)) + println("neutronUser lp token bal: ", neutronUserLPTokenBal) + }) + + t.Run("init covenant", func(t *testing.T) { + presetIbcFee := PresetIbcFee{ + AckFee: "100000", + TimeoutFee: "100000", + } + + timeouts := Timeouts{ + IcaTimeout: "10000", // sec + IbcTransferTimeout: "10000", // sec + } + + contractCodes := ContractCodeIds{ + IbcForwarderCode: ibcForwarderCodeId, + ClockCode: clockCodeId, + HolderCode: holderCodeId, + LiquidPoolerCode: lperCodeId, + LiquidStakerCode: liquidStakerCodeId, + NativeSplitterCode: remoteChainSplitterCodeId, + } + currentHeight := testCtx.GetNeutronHeight() + + lockupBlock := Block(currentHeight + 110) + lockupConfig := Expiration{ + AtHeight: &lockupBlock, + } + + lsInfo := LsInfo{ + LsDenom: nativeStatomDenom, + LsDenomOnNeutron: neutronStatomIbcDenom, + LsChainToNeutronChannelId: testCtx.StrideTransferChannelIds[testCtx.Neutron.Config().Name], + LsNeutronConnectionId: neutronStrideIBCConnId, + } + + lsContribution := Coin{ + Denom: nativeAtomDenom, + Amount: "2500000000", + } + liquidPoolerContribution := Coin{ + Denom: nativeAtomDenom, + Amount: "2500000000", + } + + lsForwarderConfig := CovenantPartyConfig{ + Interchain: &InterchainCovenantParty{ + Addr: neutronUser.Bech32Address(cosmosNeutron.Config().Bech32Prefix), + NativeDenom: neutronAtomIbcDenom, + RemoteChainDenom: nativeAtomDenom, + PartyToHostChainChannelId: testCtx.GaiaTransferChannelIds[cosmosStride.Config().Name], + HostToPartyChainChannelId: testCtx.StrideTransferChannelIds[cosmosAtom.Config().Name], + PartyReceiverAddr: neutronUser.Bech32Address(cosmosNeutron.Config().Bech32Prefix), + PartyChainConnectionId: neutronAtomIBCConnId, + IbcTransferTimeout: timeouts.IbcTransferTimeout, + Contribution: lsContribution, + }, + } + + liquidPoolerForwarderConfig := CovenantPartyConfig{ + Interchain: &InterchainCovenantParty{ + Addr: neutronUser.Bech32Address(cosmosNeutron.Config().Bech32Prefix), + NativeDenom: neutronAtomIbcDenom, + RemoteChainDenom: nativeAtomDenom, + PartyToHostChainChannelId: testCtx.GaiaTransferChannelIds[cosmosNeutron.Config().Name], + HostToPartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name], + PartyReceiverAddr: neutronUser.Bech32Address(cosmosNeutron.Config().Bech32Prefix), + PartyChainConnectionId: neutronAtomIBCConnId, + IbcTransferTimeout: timeouts.IbcTransferTimeout, + Contribution: liquidPoolerContribution, + }, + } + + pairType := PairType{ + Stable: struct{}{}, + } + + nativeSplitterConfig := NativeSplitterConfig{ + ChannelId: testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name], + ConnectionId: neutronAtomIBCConnId, + Denom: nativeAtomDenom, + Amount: strconv.FormatUint(atomContributionAmount, 10), + LsShare: "0.5", + NativeShare: "0.5", + } + + covenantInstantiationMsg := CovenantInstantiationMsg{ + Label: "single_party_pol_covenant", + Timeouts: timeouts, + PresetIbcFee: presetIbcFee, + ContractCodeIds: contractCodes, + LockupConfig: lockupConfig, + PoolAddress: stableswapAddress, + LsInfo: lsInfo, + PartyASingleSideLimit: "10000000", + PartyBSingleSideLimit: "10000000", + LsForwarderConfig: lsForwarderConfig, + LpForwarderConfig: liquidPoolerForwarderConfig, + ExpectedPoolRatio: "1.0", + AcceptablePoolRatioDelta: "0.1", + PairType: pairType, + NativeSplitterConfig: nativeSplitterConfig, + } + + covenantAddress = testCtx.ManualInstantiateLS(covenantCodeId, covenantInstantiationMsg, neutronUser, keyring.BackendTest) + println("covenant address: ", covenantAddress) + }) + + t.Run("query covenant contracts", func(t *testing.T) { + clockAddress = testCtx.QueryClockAddress(covenantAddress) + holderAddress = testCtx.QueryHolderAddress(covenantAddress) + liquidPoolerAddress = testCtx.QueryLiquidPoolerAddress(covenantAddress) + liquidStakerAddress = testCtx.QueryLiquidStakerAddress(covenantAddress) + lsForwarderAddress = testCtx.QueryIbcForwarderTyAddress(covenantAddress, "ls") + liquidPoolerForwarderAddress = testCtx.QueryIbcForwarderTyAddress(covenantAddress, "lp") + remoteChainSplitterAddress = testCtx.QueryRemoteChainSplitterAddress(covenantAddress) + }) + + t.Run("fund contracts with neutron", func(t *testing.T) { + addrs := []string{ + clockAddress, + holderAddress, + liquidPoolerAddress, + liquidStakerAddress, + lsForwarderAddress, + liquidPoolerForwarderAddress, + remoteChainSplitterAddress, + } + + testCtx.FundChainAddrs(addrs, cosmosNeutron, neutronUser, 5000000000) + }) + + t.Run("tick until forwarders create ica", func(t *testing.T) { + for { + testCtx.TickStride(clockAddress, keyring.BackendTest, neutronUser.KeyName) + lsForwarderState := testCtx.QueryContractState(lsForwarderAddress) + println("lsForwarderState: ", lsForwarderState) + + liquidPoolerForwarderState := testCtx.QueryContractState(liquidPoolerForwarderAddress) + println("liquidPoolerForwarderState: ", liquidPoolerForwarderState) + + splitterState := testCtx.QueryContractState(remoteChainSplitterAddress) + println("splitterState: ", splitterState) + + liquidStakerState := testCtx.QueryContractState(liquidStakerAddress) + println("liquidStakerState: ", liquidStakerState) + + if splitterState == "ica_created" && lsForwarderState == "ica_created" && liquidPoolerForwarderState == "ica_created" && liquidStakerState == "ica_created" { + partyDepositAddress = testCtx.QueryDepositAddressSingleParty(covenantAddress) + strideIcaAddress = testCtx.QueryContractICA(liquidStakerAddress) + lsForwarderIcaAddress = testCtx.QueryContractDepositAddress(lsForwarderAddress) + liquidPoolerForwarderIcaAddress = testCtx.QueryContractDepositAddress(liquidPoolerForwarderAddress) + println("ls forwarder ica address: ", lsForwarderIcaAddress) + println("liquid pooler forwarder ica address: ", liquidPoolerForwarderIcaAddress) + break + } + } + }) + + t.Run("fund the forwarders with sufficient funds", func(t *testing.T) { + testCtx.FundChainAddrs([]string{partyDepositAddress}, cosmosAtom, gaiaUser, int64(atomContributionAmount)) + + testCtx.SkipBlocksStride(3) + }) + + t.Run("tick until splitter splits the funds to ls and lp forwarders", func(t *testing.T) { + for { + testCtx.TickStride(remoteChainSplitterAddress, keyring.BackendTest, neutronUser.KeyName) + + lsForwarderIcaAtomBal := testCtx.QueryHubDenomBalance(nativeAtomDenom, lsForwarderIcaAddress) + lpForwarderIcaAtomBal := testCtx.QueryHubDenomBalance(nativeAtomDenom, liquidPoolerForwarderIcaAddress) + splitterAtomBalance := testCtx.QueryHubDenomBalance(nativeAtomDenom, partyDepositAddress) + + println("ls forwarder ica atom balance: ", lsForwarderIcaAtomBal) + println("lp forwarder ica atom balance: ", lpForwarderIcaAtomBal) + println("splitter atom balance: ", splitterAtomBalance) + + if lsForwarderIcaAtomBal != 0 && lpForwarderIcaAtomBal != 0 { + println("liquid pooler received atom & statom") + break + } + } + }) + + getLsPermisionlessTransferMsg := func(amount uint64) []string { + // Construct a transfer message + msg := TransferExecutionMsg{ + Transfer: TransferAmount{ + Amount: amount, + }, + } + transferMsgJson, err := json.Marshal(msg) + require.NoError(t, err) + + // transfer command for permissionless transfer from stride ica to lper + transferCmd := []string{"neutrond", "tx", "wasm", "execute", liquidStakerAddress, + string(transferMsgJson), + "--from", neutronUser.KeyName, + "--gas-prices", "0.0untrn", + "--gas-adjustment", `1.8`, + "--output", "json", + "--home", testCtx.Neutron.HomeDir(), + "--node", testCtx.Neutron.GetRPCAddress(), + "--chain-id", testCtx.Neutron.Config().ChainID, + "--gas", "42069420", + "--keyring-backend", keyring.BackendTest, + "-y", + } + return transferCmd + } + var strideIcaStatomBal uint64 + + t.Run("tick until liquid staker liquid stakes", func(t *testing.T) { + for { + testCtx.TickStride(clockAddress, keyring.BackendTest, neutronUser.KeyName) + + liquidPoolerStatomBal := testCtx.QueryNeutronDenomBalance(neutronStatomIbcDenom, liquidPoolerAddress) + lperAtomBal := testCtx.QueryNeutronDenomBalance(neutronAtomIbcDenom, liquidPoolerAddress) + strideIcaStatomBal = testCtx.QueryStrideDenomBalance(nativeStatomDenom, strideIcaAddress) + + println("lper statom balance: ", liquidPoolerStatomBal) + println("lper atom balance: ", lperAtomBal) + println("stride ica statom balance: ", strideIcaStatomBal) + + if strideIcaStatomBal != 0 { + println("stride ICA received statom: ", strideIcaStatomBal) + break + } + } + }) + + t.Run("permisionless forward", func(t *testing.T) { + testCtx.SkipBlocksStride(10) + permisionlessTransferMsg := getLsPermisionlessTransferMsg(strideIcaStatomBal) + txOut, _, _ := testCtx.Neutron.Exec(testCtx.Ctx, permisionlessTransferMsg, nil) + println("permisionless transfer msg tx hash: ", string(txOut)) + testCtx.SkipBlocksStride(10) + }) + + t.Run("tick until liquid pooler provides liquidity", func(t *testing.T) { + for { + liquidPoolerLpTokenBal := testCtx.QueryLpTokenBalance(liquidityTokenAddress, liquidPoolerAddress) + holderLpTokenBal := testCtx.QueryLpTokenBalance(liquidityTokenAddress, holderAddress) + neutronUserLpTokenBal := testCtx.QueryLpTokenBalance(liquidityTokenAddress, neutronUser.Bech32Address(cosmosNeutron.Config().Bech32Prefix)) + println("liquidPoolerLpTokenBal: ", liquidPoolerLpTokenBal) + println("holderLpTokenBal: ", holderLpTokenBal) + println("neutronUserLpTokenBal: ", neutronUserLpTokenBal) + + if liquidPoolerLpTokenBal == 0 { + testCtx.TickStride(clockAddress, keyring.BackendTest, neutronUser.KeyName) + } else { + break + } + } + }) + + t.Run("user redeems lp tokens for underlying liquidity", func(t *testing.T) { + testCtx.SkipBlocksStride(10) + cmd := []string{"neutrond", "tx", "wasm", "execute", holderAddress, + `{"claim":{}}`, + "--from", neutronUser.GetKeyName(), + "--gas-prices", "0.0untrn", + "--gas-adjustment", `1.5`, + "--output", "json", + "--node", testCtx.Neutron.GetRPCAddress(), + "--home", testCtx.Neutron.HomeDir(), + "--chain-id", testCtx.Neutron.Config().ChainID, + "--gas", "42069420", + "--keyring-backend", keyring.BackendTest, + "-y", + } + + resp, _, err := testCtx.Neutron.Exec(testCtx.Ctx, cmd, nil) + require.NoError(testCtx.T, err, "claim failed") + println("claim response: ", string(resp)) + testCtx.SkipBlocksStride(5) + + for { + stAtomBal := testCtx.QueryNeutronDenomBalance(neutronStatomIbcDenom, neutronUser.Bech32Address(cosmosNeutron.Config().Bech32Prefix)) + atomBal := testCtx.QueryNeutronDenomBalance(neutronAtomIbcDenom, neutronUser.Bech32Address(cosmosNeutron.Config().Bech32Prefix)) + println("neutron user stAtomBal: ", stAtomBal) + println("neutron user atomBal: ", atomBal) + if stAtomBal != 0 && atomBal != 0 { + break + } else { + testCtx.TickStride(clockAddress, keyring.BackendTest, neutronUser.KeyName) + } + } + }) + }) +} diff --git a/interchaintest/single-party-pol/types.go b/interchaintest/single-party-pol/types.go new file mode 100644 index 00000000..136b22e8 --- /dev/null +++ b/interchaintest/single-party-pol/types.go @@ -0,0 +1,364 @@ +package covenant_single_party_pol + +type Validator struct { + Name string `json:"name"` + Address string `json:"address"` + Weight int `json:"weight"` +} + +type Data struct { + BlockHeight string `json:"block_height"` + Total string `json:"total"` + Validators []Validator `json:"validators"` +} + +// astroport stableswap +type StableswapInstantiateMsg struct { + TokenCodeId uint64 `json:"token_code_id"` + FactoryAddr string `json:"factory_addr"` + AssetInfos []AssetInfo `json:"asset_infos"` + InitParams []byte `json:"init_params"` +} + +type AssetInfo struct { + Token *Token `json:"token,omitempty"` + NativeToken *NativeToken `json:"native_token,omitempty"` +} + +type StablePoolParams struct { + Amp uint64 `json:"amp"` + Owner *string `json:"owner"` +} + +type Token struct { + ContractAddr string `json:"contract_addr"` +} + +type NativeToken struct { + Denom string `json:"denom"` +} + +type CwCoin struct { + Denom string `json:"denom"` + Amount string `json:"amount"` +} + +// astroport factory +type FactoryInstantiateMsg struct { + PairConfigs []PairConfig `json:"pair_configs"` + TokenCodeId uint64 `json:"token_code_id"` + FeeAddress *string `json:"fee_address"` + GeneratorAddress *string `json:"generator_address"` + Owner string `json:"owner"` + WhitelistCodeId uint64 `json:"whitelist_code_id"` + CoinRegistryAddress string `json:"coin_registry_address"` +} + +type PairConfig struct { + CodeId uint64 `json:"code_id"` + PairType PairType `json:"pair_type"` + TotalFeeBps uint64 `json:"total_fee_bps"` + MakerFeeBps uint64 `json:"maker_fee_bps"` + IsDisabled bool `json:"is_disabled"` + IsGeneratorDisabled bool `json:"is_generator_disabled"` +} + +type PairType struct { + // Xyk struct{} `json:"xyk,omitempty"` + Stable struct{} `json:"stable,omitempty"` + // Custom struct{} `json:"custom,omitempty"` +} + +// astroport native coin registry + +type NativeCoinRegistryInstantiateMsg struct { + Owner string `json:"owner"` +} + +type AddExecuteMsg struct { + Add Add `json:"add"` +} + +type Add struct { + NativeCoins []NativeCoin `json:"native_coins"` +} + +type NativeCoin struct { + Name string `json:"name"` + Value uint8 `json:"value"` +} + +// Add { native_coins: Vec<(String, u8)> }, + +// astroport native token +type NativeTokenInstantiateMsg struct { + Name string `json:"name"` + Symbol string `json:"symbol"` + Decimals uint8 `json:"decimals"` + InitialBalances []Cw20Coin `json:"initial_balances"` + Mint *MinterResponse `json:"mint"` + Marketing *InstantiateMarketingInfo `json:"marketing"` +} + +type Cw20Coin struct { + Address string `json:"address"` + Amount uint64 `json:"amount"` +} + +type MinterResponse struct { + Minter string `json:"minter"` + Cap *uint64 `json:"cap,omitempty"` +} + +type InstantiateMarketingInfo struct { + Project string `json:"project"` + Description string `json:"description"` + Marketing string `json:"marketing"` + Logo Logo `json:"logo"` +} + +type Logo struct { + Url string `json:"url"` +} + +// astroport whitelist +type WhitelistInstantiateMsg struct { + Admins []string `json:"admins"` + Mutable bool `json:"mutable"` +} + +type ProvideLiqudityMsg struct { + ProvideLiquidity ProvideLiquidityStruct `json:"provide_liquidity"` +} + +type ProvideLiquidityStruct struct { + Assets []AstroportAsset `json:"assets"` + SlippageTolerance string `json:"slippage_tolerance"` + AutoStake bool `json:"auto_stake"` + Receiver string `json:"receiver"` +} + +// factory + +type FactoryPairResponse struct { + Data PairInfo `json:"data"` +} + +type LpPositionQueryResponse struct { + Data string `json:"data"` +} + +type AstroportAsset struct { + Info AssetInfo `json:"info"` + Amount string `json:"amount"` +} + +type LpPositionQuery struct{} + +type PairInfo struct { + LiquidityToken string `json:"liquidity_token"` + ContractAddr string `json:"contract_addr"` + PairType PairType `json:"pair_type"` + AssetInfos []AssetInfo `json:"asset_infos"` +} + +type LPPositionQuery struct { + LpPosition LpPositionQuery `json:"lp_position"` +} + +type Pair struct { + AssetInfos []AssetInfo `json:"asset_infos"` +} + +type PairQuery struct { + Pair Pair `json:"pair"` +} + +type CreatePair struct { + PairType PairType `json:"pair_type"` + AssetInfos []AssetInfo `json:"asset_infos"` + InitParams []byte `json:"init_params"` +} + +type CreatePairMsg struct { + CreatePair CreatePair `json:"create_pair"` +} + +type BalanceResponse struct { + Balance string `json:"balance"` +} + +type Cw20BalanceResponse struct { + Data BalanceResponse `json:"data"` +} + +type AllAccountsResponse struct { + Data []string `json:"all_accounts_response"` +} + +type Cw20QueryMsg struct { + Balance Balance `json:"balance"` + // AllAccounts *AllAccounts `json:"all_accounts"` +} + +type AllAccounts struct { +} + +type Balance struct { + Address string `json:"address"` +} + +type NativeBalQueryResponse struct { + Amount string `json:"amount"` + Denom string `json:"denom"` +} + +// single party POL types + +type CovenantInstantiationMsg struct { + Label string `json:"label"` + Timeouts Timeouts `json:"timeouts"` + PresetIbcFee PresetIbcFee `json:"preset_ibc_fee"` + ContractCodeIds ContractCodeIds `json:"contract_codes"` + TickMaxGas string `json:"clock_tick_max_gas,omitempty"` + LockupConfig Expiration `json:"lockup_period"` + PoolAddress string `json:"pool_address"` + LsInfo LsInfo `json:"ls_info"` + PartyASingleSideLimit string `json:"party_a_single_side_limit"` + PartyBSingleSideLimit string `json:"party_b_single_side_limit"` + LsForwarderConfig CovenantPartyConfig `json:"ls_forwarder_config"` + LpForwarderConfig CovenantPartyConfig `json:"lp_forwarder_config"` + ExpectedPoolRatio string `json:"expected_pool_ratio"` + AcceptablePoolRatioDelta string `json:"acceptable_pool_ratio_delta"` + PairType PairType `json:"pool_pair_type"` + NativeSplitterConfig NativeSplitterConfig `json:"native_splitter_config"` +} + +type NativeSplitterConfig struct { + ChannelId string `json:"channel_id"` + ConnectionId string `json:"connection_id"` + Denom string `json:"denom"` + Amount string `json:"amount"` + LsShare string `json:"ls_share"` + NativeShare string `json:"native_share"` +} + +type CovenantPartyConfig struct { + Interchain *InterchainCovenantParty `json:"interchain,omitempty"` + Native *NativeCovenantParty `json:"native,omitempty"` +} + +type Coin struct { + Denom string `json:"denom"` + Amount string `json:"amount"` +} + +type InterchainCovenantParty struct { + Addr string `json:"addr"` + NativeDenom string `json:"native_denom"` + RemoteChainDenom string `json:"remote_chain_denom"` + PartyToHostChainChannelId string `json:"party_to_host_chain_channel_id"` + HostToPartyChainChannelId string `json:"host_to_party_chain_channel_id"` + PartyReceiverAddr string `json:"party_receiver_addr"` + PartyChainConnectionId string `json:"party_chain_connection_id"` + IbcTransferTimeout string `json:"ibc_transfer_timeout"` + Contribution Coin `json:"contribution"` +} + +type NativeCovenantParty struct { + Addr string `json:"addr"` + NativeDenom string `json:"native_denom"` + PartyReceiverAddr string `json:"party_receiver_addr"` + Contribution Coin `json:"contribution"` +} + +type LsInfo struct { + LsDenom string `json:"ls_denom"` + LsDenomOnNeutron string `json:"ls_denom_on_neutron"` + LsChainToNeutronChannelId string `json:"ls_chain_to_neutron_channel_id"` + LsNeutronConnectionId string `json:"ls_neutron_connection_id"` +} + +type Timeouts struct { + IcaTimeout string `json:"ica_timeout"` + IbcTransferTimeout string `json:"ibc_transfer_timeout"` +} + +type PresetIbcFee struct { + AckFee string `json:"ack_fee"` + TimeoutFee string `json:"timeout_fee"` +} + +type Timestamp string +type Block uint64 + +type Expiration struct { + Never string `json:"none,omitempty"` + AtHeight *Block `json:"at_height,omitempty"` + AtTime *Timestamp `json:"at_time,omitempty"` +} + +type ContractCodeIds struct { + IbcForwarderCode uint64 `json:"ibc_forwarder_code"` + ClockCode uint64 `json:"clock_code"` + HolderCode uint64 `json:"holder_code"` + LiquidPoolerCode uint64 `json:"liquid_pooler_code"` + LiquidStakerCode uint64 `json:"liquid_staker_code"` + NativeSplitterCode uint64 `json:"native_splitter_code"` +} + +type SplitType struct { + Custom SplitConfig `json:"custom"` +} + +type DenomSplit struct { + Denom string `json:"denom"` + Type SplitType `json:"split"` +} + +type SplitConfig struct { + Receivers map[string]string `json:"receivers"` +} + +type LiquidStakerInstantiateMsg struct { + ClockAddress string `json:"clock_address"` + StrideNeutronIbcTransferChannelID string `json:"stride_neutron_ibc_transfer_channel_id"` + NeutronStrideIbcConnectionID string `json:"neutron_stride_ibc_connection_id"` + NextContract string `json:"next_contract"` + LsDenom string `json:"ls_denom"` + IbcFee IbcFee `json:"ibc_fee"` // Assuming IbcFee is defined elsewhere + IcaTimeout string `json:"ica_timeout"` + IbcTransferTimeout string `json:"ibc_transfer_timeout"` + AutopilotFormat string `json:"autopilot_format"` +} + +type IbcFee struct { + RecvFee []CwCoin `json:"recv_fee"` + AckFee []CwCoin `json:"ack_fee"` + TimeoutFee []CwCoin `json:"timeout_fee"` +} + +////////////////////////////////////////////// +///// Ls contract +////////////////////////////////////////////// + +// Execute +type TransferExecutionMsg struct { + Transfer TransferAmount `json:"transfer"` +} + +// Rust type here is Uint128 which can't safely be serialized +// to json int. It needs to go as a string over the wire. +type TransferAmount struct { + Amount uint64 `json:"amount,string"` +} + +// Queries +type LsIcaQuery struct { + StrideIca StrideIcaQuery `json:"stride_i_c_a"` +} +type StrideIcaQuery struct{} + +type StrideIcaQueryResponse struct { + Addr string `json:"data"` +} diff --git a/interchaintest/utils/connection_helpers.go b/interchaintest/utils/connection_helpers.go index 7ccb700b..ab1dbca8 100644 --- a/interchaintest/utils/connection_helpers.go +++ b/interchaintest/utils/connection_helpers.go @@ -19,22 +19,30 @@ import ( ) type TestContext struct { - Neutron *cosmos.CosmosChain - Hub *cosmos.CosmosChain - Osmosis *cosmos.CosmosChain - OsmoClients []*ibc.ClientOutput - GaiaClients []*ibc.ClientOutput - NeutronClients []*ibc.ClientOutput - OsmoConnections []*ibc.ConnectionOutput - GaiaConnections []*ibc.ConnectionOutput - NeutronConnections []*ibc.ConnectionOutput + Neutron *cosmos.CosmosChain + Hub *cosmos.CosmosChain + Osmosis *cosmos.CosmosChain + Stride *cosmos.CosmosChain + + OsmoClients []*ibc.ClientOutput + GaiaClients []*ibc.ClientOutput + NeutronClients []*ibc.ClientOutput + StrideClients []*ibc.ClientOutput + + OsmoConnections []*ibc.ConnectionOutput + GaiaConnections []*ibc.ConnectionOutput + NeutronConnections []*ibc.ConnectionOutput + StrideConnections []*ibc.ConnectionOutput + NeutronTransferChannelIds map[string]string GaiaTransferChannelIds map[string]string OsmoTransferChannelIds map[string]string - GaiaIcsChannelIds map[string]string - NeutronIcsChannelIds map[string]string - T *testing.T - Ctx context.Context + StrideTransferChannelIds map[string]string + + GaiaIcsChannelIds map[string]string + NeutronIcsChannelIds map[string]string + T *testing.T + Ctx context.Context } func (testCtx *TestContext) Tick(clock string, keyring string, from string) { @@ -60,10 +68,40 @@ func (testCtx *TestContext) Tick(clock string, keyring string, from string) { testCtx.SkipBlocks(3) } -func (testCtx *TestContext) SkipBlocks(n int) { +func (testCtx *TestContext) TickStride(clock string, keyring string, from string) { + neutronHeight, _ := testCtx.Neutron.Height(testCtx.Ctx) + println("tick neutron@", neutronHeight) + cmd := []string{"neutrond", "tx", "wasm", "execute", clock, + `{"tick":{}}`, + "--gas-prices", "0.0untrn", + "--gas-adjustment", `1.5`, + "--output", "json", + "--node", testCtx.Neutron.GetRPCAddress(), + "--home", testCtx.Neutron.HomeDir(), + "--chain-id", testCtx.Neutron.Config().ChainID, + "--from", from, + "--gas", "4500000", + "--keyring-backend", keyring, + "-y", + } + + tickresponse, _, err := testCtx.Neutron.Exec(testCtx.Ctx, cmd, nil) + require.NoError(testCtx.T, err) + println("tick reponse: ", string(tickresponse)) + testCtx.SkipBlocksStride(3) +} + +func (testCtx *TestContext) SkipBlocks(n uint64) { + require.NoError( + testCtx.T, + testutil.WaitForBlocks(testCtx.Ctx, int(n), testCtx.Hub, testCtx.Neutron, testCtx.Osmosis), + "failed to wait for blocks") +} + +func (testCtx *TestContext) SkipBlocksStride(n uint64) { require.NoError( testCtx.T, - testutil.WaitForBlocks(testCtx.Ctx, n, testCtx.Hub, testCtx.Neutron, testCtx.Osmosis), + testutil.WaitForBlocks(testCtx.Ctx, int(n), testCtx.Hub, testCtx.Neutron, testCtx.Stride), "failed to wait for blocks") } @@ -98,6 +136,8 @@ func (testCtx *TestContext) GetChainClients(chain string) []*ibc.ClientOutput { return testCtx.GaiaClients case "osmosis-3": return testCtx.OsmoClients + case "stride-3": + return testCtx.StrideClients default: return ibc.ClientOutputs{} } @@ -111,6 +151,8 @@ func (testCtx *TestContext) SetTransferChannelId(chain string, destChain string, testCtx.GaiaTransferChannelIds[destChain] = channelId case "osmosis-3": testCtx.OsmoTransferChannelIds[destChain] = channelId + case "stride-3": + testCtx.StrideTransferChannelIds[destChain] = channelId default: } } @@ -133,6 +175,8 @@ func (testCtx *TestContext) UpdateChainClients(chain string, clients []*ibc.Clie testCtx.GaiaClients = clients case "osmosis-3": testCtx.OsmoClients = clients + case "stride-3": + testCtx.StrideClients = clients default: } } @@ -145,6 +189,8 @@ func (testCtx *TestContext) GetChainConnections(chain string) []*ibc.ConnectionO return testCtx.GaiaConnections case "osmosis-3": return testCtx.OsmoConnections + case "stride-3": + return testCtx.StrideConnections default: println("error finding connections for chain ", chain) return []*ibc.ConnectionOutput{} @@ -159,6 +205,8 @@ func (testCtx *TestContext) UpdateChainConnections(chain string, connections []* testCtx.GaiaConnections = connections case "osmosis-3": testCtx.OsmoConnections = connections + case "stride-3": + testCtx.StrideConnections = connections default: } } @@ -487,7 +535,7 @@ func (testCtx *TestContext) QueryHubDenomBalance(denom string, addr string) uint require.NoError(testCtx.T, err, "failed to get hub denom balance") uintBal := uint64(bal) - println(addr, " balance: (", denom, ",", uintBal, ")") + // println(addr, " balance: (", denom, ",", uintBal, ")") return uintBal } @@ -496,7 +544,14 @@ func (testCtx *TestContext) QueryOsmoDenomBalance(denom string, addr string) uin require.NoError(testCtx.T, err, "failed to get osmosis denom balance") uintBal := uint64(bal) - // println(addr, " balance: (", denom, ",", uintBal, ")") + return uintBal +} + +func (testCtx *TestContext) QueryStrideDenomBalance(denom string, addr string) uint64 { + bal, err := testCtx.Stride.GetBalance(testCtx.Ctx, addr, denom) + require.NoError(testCtx.T, err, "failed to get stride denom balance") + + uintBal := uint64(bal) return uintBal } @@ -564,6 +619,48 @@ func (testCtx *TestContext) QueryIbcForwarderAddress(contract string, party stri return response.Data } +func (testCtx *TestContext) QueryIbcForwarderTyAddress(contract string, ty string) string { + var response CovenantAddressQueryResponse + + type Type struct { + Type string `json:"ty"` + } + type IbcForwarderQuery struct { + Type Type `json:"ibc_forwarder_address"` + } + query := IbcForwarderQuery{ + Type: Type{ + Type: ty, + }, + } + err := testCtx.Neutron.QueryContract(testCtx.Ctx, contract, query, &response) + require.NoError( + testCtx.T, + err, + "failed to query ibc forwarder address", + ) + println(ty, " forwarder addr: ", response.Data) + return response.Data +} + +func (testCtx *TestContext) QueryRemoteChainSplitterAddress(contract string) string { + var response CovenantAddressQueryResponse + + type SplitterAddress struct{} + type SplitterAddressQuery struct { + SplitterAddress SplitterAddress `json:"splitter_address"` + } + query := SplitterAddressQuery{} + err := testCtx.Neutron.QueryContract(testCtx.Ctx, contract, query, &response) + require.NoError( + testCtx.T, + err, + "failed to query splitter address", + ) + println("splitter addr: ", response.Data) + return response.Data +} + type Party struct { Party string `json:"party"` } @@ -685,6 +782,69 @@ func (testCtx *TestContext) QueryDepositAddress(covenant string, party string) s return depositAddressResponse.Data } +func (testCtx *TestContext) QueryDepositAddressSingleParty(covenant string) string { + var depositAddressResponse CovenantAddressQueryResponse + + type PartyDepositAddress struct{} + type PartyDepositAddressQuery struct { + PartyDepositAddress PartyDepositAddress `json:"party_deposit_address"` + } + depositAddressQuery := PartyDepositAddressQuery{ + PartyDepositAddress: PartyDepositAddress{}, + } + + err := testCtx.Neutron.QueryContract(testCtx.Ctx, covenant, depositAddressQuery, &depositAddressResponse) + require.NoError( + testCtx.T, + err, + "failed to query party deposit address", + ) + println("party deposit address: ", depositAddressResponse.Data) + return depositAddressResponse.Data +} + +func (testCtx *TestContext) QueryContractDepositAddress(contract string) string { + var depositAddressResponse CovenantAddressQueryResponse + + type DepositAddress struct{} + type DepositAddressQuery struct { + DepositAddress DepositAddress `json:"deposit_address"` + } + depositAddressQuery := DepositAddressQuery{ + DepositAddress: DepositAddress{}, + } + + err := testCtx.Neutron.QueryContract(testCtx.Ctx, contract, depositAddressQuery, &depositAddressResponse) + require.NoError( + testCtx.T, + err, + "failed to query contract deposit address", + ) + println("contract deposit address: ", depositAddressResponse.Data) + return depositAddressResponse.Data +} + +func (testCtx *TestContext) QueryContractICA(contract string) string { + var icaAddressResponse CovenantAddressQueryResponse + + type IcaAddress struct{} + type IcaAddressQuery struct { + IcaAddress IcaAddress `json:"ica_address"` + } + icaAddressQuery := IcaAddressQuery{ + IcaAddress: IcaAddress{}, + } + + err := testCtx.Neutron.QueryContract(testCtx.Ctx, contract, icaAddressQuery, &icaAddressResponse) + require.NoError( + testCtx.T, + err, + "failed to query contract ica address", + ) + println("contract deposit address: ", icaAddressResponse.Data) + return icaAddressResponse.Data +} + func (testCtx *TestContext) ManualInstantiate(codeId uint64, msg any, from *ibc.Wallet, keyring string) string { codeIdStr := strconv.FormatUint(codeId, 10) @@ -740,6 +900,61 @@ func (testCtx *TestContext) ManualInstantiate(codeId uint64, msg any, from *ibc. return covenantAddress } +func (testCtx *TestContext) ManualInstantiateLS(codeId uint64, msg any, from *ibc.Wallet, keyring string) string { + codeIdStr := strconv.FormatUint(codeId, 10) + + str, err := json.Marshal(msg) + require.NoError(testCtx.T, err, "Failed to marshall CovenantInstantiateMsg") + instantiateMsg := string(str) + + cmd := []string{"neutrond", "tx", "wasm", "instantiate", codeIdStr, + instantiateMsg, + "--label", fmt.Sprintf("covenant-%s", codeIdStr), + "--no-admin", + "--from", from.KeyName, + "--output", "json", + "--home", testCtx.Neutron.HomeDir(), + "--node", testCtx.Neutron.GetRPCAddress(), + "--chain-id", testCtx.Neutron.Config().ChainID, + "--gas", "900090000", + "--keyring-backend", keyring, + "-y", + } + + prettyJson, _ := json.MarshalIndent(msg, "", " ") + println("covenant instantiation message:") + fmt.Println(string(prettyJson)) + + covInstantiationResp, _, err := testCtx.Neutron.Exec(testCtx.Ctx, cmd, nil) + require.NoError(testCtx.T, err, "manual instantiation failed") + println("covenant instantiation response: ", string(covInstantiationResp)) + require.NoError(testCtx.T, + testutil.WaitForBlocks(testCtx.Ctx, 10, testCtx.Hub, testCtx.Neutron, testCtx.Stride)) + + queryCmd := []string{"neutrond", "query", "wasm", + "list-contract-by-code", codeIdStr, + "--output", "json", + "--home", testCtx.Neutron.HomeDir(), + "--node", testCtx.Neutron.GetRPCAddress(), + "--chain-id", testCtx.Neutron.Config().ChainID, + } + + queryResp, _, err := testCtx.Neutron.Exec(testCtx.Ctx, queryCmd, nil) + require.NoError(testCtx.T, err, "failed to query") + + type QueryContractResponse struct { + Contracts []string `json:"contracts"` + Pagination any `json:"pagination"` + } + + contactsRes := QueryContractResponse{} + require.NoError(testCtx.T, json.Unmarshal(queryResp, &contactsRes), "failed to unmarshal contract response") + + covenantAddress := contactsRes.Contracts[len(contactsRes.Contracts)-1] + + return covenantAddress +} + // astroport whitelist type WhitelistInstantiateMsg struct { Admins []string `json:"admins"` @@ -1040,6 +1255,55 @@ func (testCtx *TestContext) CreateAstroportFactoryPair(amp uint64, denom1 string testCtx.SkipBlocks(3) } +func (testCtx *TestContext) CreateAstroportFactoryPairStride(amp uint64, denom1 string, denom2 string, factory string, from *ibc.Wallet, keyring string) { + initParams := StablePoolParams{ + Amp: amp, + } + binaryData, _ := json.Marshal(initParams) + + createPairMsg := CreatePairMsg{ + CreatePair: CreatePair{ + PairType: PairType{ + Stable: struct{}{}, + }, + AssetInfos: []AssetInfo{ + { + NativeToken: &NativeToken{ + Denom: denom1, + }, + }, + { + NativeToken: &NativeToken{ + Denom: denom2, + }, + }, + }, + InitParams: binaryData, + }, + } + + str, _ := json.Marshal(createPairMsg) + + createCmd := []string{"neutrond", "tx", "wasm", "execute", + factory, + string(str), + "--gas-prices", "0.0untrn", + "--gas-adjustment", `1.5`, + "--output", "json", + "--node", testCtx.Neutron.GetRPCAddress(), + "--home", testCtx.Neutron.HomeDir(), + "--chain-id", testCtx.Neutron.Config().ChainID, + "--from", from.KeyName, + "--gas", "auto", + "--keyring-backend", keyring, + "-y", + } + + _, _, err := testCtx.Neutron.Exec(testCtx.Ctx, createCmd, nil) + require.NoError(testCtx.T, err, err) + testCtx.SkipBlocksStride(3) +} + func (testCtx *TestContext) QueryLpTokenBalance(token string, addr string) uint64 { bal := Balance{ Address: addr, @@ -1105,21 +1369,20 @@ func (testCtx *TestContext) QueryLiquidPoolerAddress(contract string) string { return response.Data } -type LatestPoolState struct{} -type LatestPoolStateQuery struct { - LatestPoolState LatestPoolState `json:"latest_pool_state"` -} - -func (testCtx *TestContext) QueryLiquidPoolerLatestPoolState(contract string) string { +func (testCtx *TestContext) QueryLiquidStakerAddress(contract string) string { var response CovenantAddressQueryResponse + type LiquidStakerAddress struct{} + type LiquidStakerQuery struct { + LiquidStakerAddress LiquidStakerAddress `json:"liquid_staker_address"` + } - err := testCtx.Neutron.QueryContract(testCtx.Ctx, contract, LatestPoolStateQuery{}, &response) + err := testCtx.Neutron.QueryContract(testCtx.Ctx, contract, LiquidStakerQuery{}, &response) require.NoError( testCtx.T, err, - "failed to query liquid pooler pool query state", + "failed to query liquid staker address", ) - println("pool query state: ", response.Data) + println("liquid staker address: ", response.Data) return response.Data } @@ -1318,3 +1581,38 @@ func (testCtx *TestContext) InstantiateCmdExecOsmo(codeId uint64, label string, return instantiatedAddress } + +func (testCtx *TestContext) GetPermisionlessLsTransferMessage(amount uint64, liquidStakerAddress string, user *ibc.Wallet, keyring string) []string { + type TransferAmount struct { + Amount uint64 `json:"amount,string"` + } + + type TransferExecutionMsg struct { + Transfer TransferAmount `json:"transfer"` + } + + // Construct a transfer message + msg := TransferExecutionMsg{ + Transfer: TransferAmount{ + Amount: amount, + }, + } + transferMsgJson, err := json.Marshal(msg) + require.NoError(testCtx.T, err) + + // transfer command for permissionless transfer from stride ica to lper + transferCmd := []string{"neutrond", "tx", "wasm", "execute", liquidStakerAddress, + string(transferMsgJson), + "--from", user.KeyName, + "--gas-prices", "0.0untrn", + "--gas-adjustment", `1.8`, + "--output", "json", + "--home", testCtx.Neutron.HomeDir(), + "--node", testCtx.Neutron.GetRPCAddress(), + "--chain-id", testCtx.Neutron.Config().ChainID, + "--gas", "42069420", + "--keyring-backend", keyring, + "-y", + } + return transferCmd +} diff --git a/interchaintest/utils/genesis_helpers.go b/interchaintest/utils/genesis_helpers.go index ccaffb7e..5095cce6 100644 --- a/interchaintest/utils/genesis_helpers.go +++ b/interchaintest/utils/genesis_helpers.go @@ -172,106 +172,106 @@ func SetupOsmoGenesis(allowed_messages []string) func(ibc.ChainConfig, []byte) ( return nil, fmt.Errorf("failed to set allow_messages for interchainaccount host in genesis json: %w. \ngenesis json: %s", err, g) } - type Fee struct { - Amount string `json:"amount"` - Denom string `json:"denom"` - } - zeroCreationFee := []Fee{ - { - Amount: "10", - Denom: "uosmo", - }, - } - - if err := dyno.Set(g, zeroCreationFee, "app_state", "poolmanager", "params", "pool_creation_fee"); err != nil { - return nil, fmt.Errorf("failed to set poolmanager pool creation fee") - } - - if err := dyno.Set(g, zeroCreationFee, "app_state", "gamm", "params", "pool_creation_fee"); err != nil { - return nil, fmt.Errorf("failed to set poolmanager pool creation fee") - } - - // Retrieve the params map - params, err := dyno.Get(g, "app_state", "concentratedliquidity", "params") - if err != nil { - return nil, fmt.Errorf("failed to get params for concentratedliquidity: %w", err) - } - - // Assert the type of the params to be map[string]interface{} - paramsMap, ok := params.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("params for concentratedliquidity is not a map") - } - - // Update only the is_permissionless_pool_creation_enabled field - paramsMap["is_permissionless_pool_creation_enabled"] = true - - // Set the modified params map back - if err := dyno.Set(g, paramsMap, "app_state", "concentratedliquidity", "params"); err != nil { - return nil, fmt.Errorf("failed to set modified params for concentratedliquidity: %w", err) - } - - // Retrieve tokenfactory params map - tokenfactoryParams, err := dyno.Get(g, "app_state", "tokenfactory", "params") - if err != nil { - return nil, fmt.Errorf("failed to get params for tokenfactory: %w", err) - } - - // Assert the type of the params to be map[string]interface{} - tokenfactoryParamsMap, ok := tokenfactoryParams.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("params for tokenfactory is not a map") - } - - // Update only the denom_creation_gas_consume field - tokenfactoryParamsMap["denom_creation_gas_consume"] = "1" + // type Fee struct { + // Amount string `json:"amount"` + // Denom string `json:"denom"` + // } + // zeroCreationFee := []Fee{ + // { + // Amount: "10", + // Denom: "uosmo", + // }, + // } + + // if err := dyno.Set(g, zeroCreationFee, "app_state", "gamm", "params", "pool_creation_fee"); err != nil { + // return nil, fmt.Errorf("failed to set poolmanager pool creation fee") + // } + + // // Retrieve tokenfactory params map + // // tokenfactoryParams, err := dyno.Get(g, "app_state", "tokenfactory", "params") + // // if err != nil { + // // return nil, fmt.Errorf("failed to get params for tokenfactory: %w", err) + // // } + + // // // Assert the type of the params to be map[string]interface{} + // // tokenfactoryParamsMap, ok := tokenfactoryParams.(map[string]interface{}) + // // if !ok { + // // return nil, fmt.Errorf("params for tokenfactory is not a map") + // // } + + // // // Update only the denom_creation_gas_consume field + // // tokenfactoryParamsMap["denom_creation_gas_consume"] = "1" + + // // // Set the modified params map back + // // if err := dyno.Set(g, tokenfactoryParamsMap, "app_state", "tokenfactory", "params"); err != nil { + // // return nil, fmt.Errorf("failed to set modified params for tokenfactory: %w", err) + // // } + + // // genutil gas_limits + // // Retrieve the gen_txs array + // genTxs, err := dyno.Get(g, "app_state", "genutil", "gen_txs") + // if err != nil { + // return nil, fmt.Errorf("failed to get gen_txs for genutil: %w", err) + // } + + // genTxsSlice, ok := genTxs.([]interface{}) + // if !ok { + // return nil, fmt.Errorf("gen_txs in genutil is not a slice") + // } + + // // Update the gas_limit for each item in the slice + // for _, genTx := range genTxsSlice { + // genTxMap, ok := genTx.(map[string]interface{}) + // if !ok { + // return nil, fmt.Errorf("gen_tx item is not a map") + // } + + // if authInfo, ok := genTxMap["auth_info"].(map[string]interface{}); ok { + // if fee, ok := authInfo["fee"].(map[string]interface{}); ok { + // fee["gas_limit"] = "350000" + // } + // } + // } + + // // Set the modified gen_txs slice back + // if err := dyno.Set(g, genTxsSlice, "app_state", "genutil", "gen_txs"); err != nil { + // return nil, fmt.Errorf("failed to set modified gen_txs for genutil: %w", err) + // } + + // if err := dyno.Set(g, "100000000", "consensus_params", "block", "max_gas"); err != nil { + // return nil, fmt.Errorf("failed to set block max gas: %w", err) + // } - // Set the modified params map back - if err := dyno.Set(g, tokenfactoryParamsMap, "app_state", "tokenfactory", "params"); err != nil { - return nil, fmt.Errorf("failed to set modified params for tokenfactory: %w", err) - } - - // genutil gas_limits - // Retrieve the gen_txs array - genTxs, err := dyno.Get(g, "app_state", "genutil", "gen_txs") + out, err := json.Marshal(g) + println("osmo genesis:") + print(string(out)) if err != nil { - return nil, fmt.Errorf("failed to get gen_txs for genutil: %w", err) - } - - genTxsSlice, ok := genTxs.([]interface{}) - if !ok { - return nil, fmt.Errorf("gen_txs in genutil is not a slice") + return nil, fmt.Errorf("failed to marshal genesis bytes to json: %w", err) } + return out, nil + } +} - // Update the gas_limit for each item in the slice - for _, genTx := range genTxsSlice { - genTxMap, ok := genTx.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("gen_tx item is not a map") - } - - if authInfo, ok := genTxMap["auth_info"].(map[string]interface{}); ok { - if fee, ok := authInfo["fee"].(map[string]interface{}); ok { - fee["gas_limit"] = "350000" - } - } +func SetupStrideGenesis(allowed_messages []string) func(ibc.ChainConfig, []byte) ([]byte, error) { + return func(chainConfig ibc.ChainConfig, genbz []byte) ([]byte, error) { + g := make(map[string]interface{}) + if err := json.Unmarshal(genbz, &g); err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis file: %w", err) } - // Set the modified gen_txs slice back - if err := dyno.Set(g, genTxsSlice, "app_state", "genutil", "gen_txs"); err != nil { - return nil, fmt.Errorf("failed to set modified gen_txs for genutil: %w", err) + if err := dyno.Set(g, true, "app_state", "autopilot", "params", "stakeibc_active"); err != nil { + return nil, fmt.Errorf("failed to set autopilot stakeibc in genesis json: %w", err) } - if err := dyno.Set(g, "100000000", "consensus_params", "block", "max_gas"); err != nil { - return nil, fmt.Errorf("failed to set block max gas: %w", err) + if err := dyno.Set(g, allowed_messages, "app_state", "interchainaccounts", "host_genesis_state", "params", "allow_messages"); err != nil { + return nil, fmt.Errorf("failed to set allow_messages for interchainaccount host in genesis json: %w", err) } out, err := json.Marshal(g) - println("osmo genesis:") - print(string(out)) if err != nil { return nil, fmt.Errorf("failed to marshal genesis bytes to json: %w", err) } + return out, nil } } diff --git a/justfile b/justfile index e9a891ec..2dcf056b 100644 --- a/justfile +++ b/justfile @@ -8,24 +8,24 @@ lint: cargo clippy --all-targets -- -D warnings optimize: - #!/usr/bin/env sh - ./optimize.sh - if [[ $(uname -m) =~ "arm64" ]]; then - for file in ./artifacts/*-aarch64.wasm; do - if [ -f "$file" ]; then - new_name="${file%-aarch64.wasm}.wasm" - mv "$file" "./$new_name" - fi - done - fi + #!/usr/bin/env sh + ./optimize.sh + if [[ $(uname -m) =~ "arm64" ]]; then + for file in ./artifacts/*-aarch64.wasm; do + if [ -f "$file" ]; then + new_name="${file%-aarch64.wasm}.wasm" + mv "$file" "./$new_name" + fi + done + fi local-e2e-rebuild TEST PATTERN='.*': optimize - mkdir -p interchaintest/{{TEST}}/wasms - cp -R interchaintest/wasms/polytone/*.wasm interchaintest/{{TEST}}/wasms - cp -R interchaintest/wasms/astroport/*.wasm interchaintest/{{TEST}}/wasms - cp -R artifacts/*.wasm interchaintest/{{TEST}}/wasms - ls interchaintest/{{TEST}}/wasms - cd interchaintest/{{TEST}} && go clean -testcache && go test -timeout 50m -v -run '{{PATTERN}}' + mkdir -p interchaintest/{{TEST}}/wasms + cp -R interchaintest/wasms/polytone/*.wasm interchaintest/{{TEST}}/wasms + cp -R interchaintest/wasms/astroport/*.wasm interchaintest/{{TEST}}/wasms + cp -R artifacts/*.wasm interchaintest/{{TEST}}/wasms + ls interchaintest/{{TEST}}/wasms + cd interchaintest/{{TEST}} && go clean -testcache && go test -timeout 50m -v -run '{{PATTERN}}' local-e2e TEST PATTERN='.*': - cd interchaintest/{{TEST}} && go clean -testcache && go test -timeout 50m -v -run '{{PATTERN}}' + cd interchaintest/{{TEST}} && go clean -testcache && go test -timeout 50m -v -run '{{PATTERN}}' diff --git a/packages/covenant-macros/src/lib.rs b/packages/covenant-macros/src/lib.rs index 1073696a..962387c8 100644 --- a/packages/covenant-macros/src/lib.rs +++ b/packages/covenant-macros/src/lib.rs @@ -129,3 +129,42 @@ pub fn covenant_next_contract(metadata: TokenStream, input: TokenStream) -> Toke .into(), ) } + +#[proc_macro_attribute] +pub fn covenant_lper_withdraw(metadata: TokenStream, input: TokenStream) -> TokenStream { + merge_variants( + metadata, + input, + quote!( + enum WithdrawMsgs { + /// Tells the LPer to withdraw his position + /// Should only be called by the holder of the covenant + Withdraw { percentage: Option }, + } + ) + .into(), + ) +} + +#[proc_macro_attribute] +pub fn covenant_holder_distribute(metadata: TokenStream, input: TokenStream) -> TokenStream { + merge_variants( + metadata, + input, + quote!( + enum DistributeMsgs { + /// After LPer finished withdrawing from LP, it sends the funds to the holder + /// and the holder distributes them based on its logic + /// Should only be called by the LPer of the covenant + Distribute {}, + /// This message is sent in case we do an IBC withdraw + /// The withdraw can fail in async way, in case that happens we want the holder to be notified on that. + /// In case of astroport, the withdraww + distribution is atomic, so nothing to worry there + /// But in case of osmosis, the withdraw is async, so the "claim" will successful happen, + /// while the withdraw can fail, in case the withdraw fails here, we execute this message on the holder + WithdrawFailed {}, + } + ) + .into(), + ) +} diff --git a/packages/covenant-utils/Cargo.toml b/packages/covenant-utils/Cargo.toml index 11a7e5a4..411eee84 100644 --- a/packages/covenant-utils/Cargo.toml +++ b/packages/covenant-utils/Cargo.toml @@ -1,11 +1,10 @@ [package] -name = "covenant-utils" -version = { workspace = true } -edition = { workspace = true } -authors = ["benskey bekauz@protonmail.com"] +name = "covenant-utils" +version = { workspace = true } +edition = { workspace = true } +authors = ["benskey bekauz@protonmail.com"] description = "A package for common utils for covenants" -license = { workspace = true } - +license = { workspace = true } [lib] @@ -16,8 +15,8 @@ cosmwasm-std = { workspace = true } prost = { workspace = true } cosmos-sdk-proto = { workspace = true } cw20 = { workspace = true } +cw-utils = { workspace = true } astroport = { workspace = true } -# polytone-note = { workspace = true } -# polytone = { workspace = true } -polytone = "1.0.0" -# polytone-note = "1.0.0" +polytone = "1.0.0" +covenant-macros = { workspace = true } +sha2 = { workspace = true } diff --git a/packages/covenant-utils/src/deadline.rs b/packages/covenant-utils/src/deadline.rs new file mode 100644 index 00000000..a720dd47 --- /dev/null +++ b/packages/covenant-utils/src/deadline.rs @@ -0,0 +1,44 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::BlockInfo; +use cw_utils::{Duration, Expiration}; + +#[cw_serde] +#[serde(untagged)] +pub enum Deadline { + Expiration(Expiration), + Duration(Duration), +} + +impl Default for Deadline { + fn default() -> Self { + Deadline::Expiration(Expiration::default()) + } +} + +impl Deadline { + pub fn into_expiration(self, block: &BlockInfo) -> Expiration { + match self { + Deadline::Expiration(expiration) => expiration, + Deadline::Duration(duration) => duration.after(block), + } + } +} + +#[cfg(test)] +mod test { + use cosmwasm_schema::cw_serde; + use cosmwasm_std::from_json; + + use super::Deadline; + + #[cw_serde] + struct Example { + expires: Deadline, + } + + #[test] + fn test() { + let json_string = "{\"expires\": {\"never\": {}}}"; + println!("{:?}", from_json::(&json_string).unwrap()); + } +} diff --git a/packages/covenant-utils/src/instantiate2_helper.rs b/packages/covenant-utils/src/instantiate2_helper.rs new file mode 100644 index 00000000..cc289a31 --- /dev/null +++ b/packages/covenant-utils/src/instantiate2_helper.rs @@ -0,0 +1,38 @@ +use cosmwasm_std::{ + instantiate2_address, Addr, Binary, CanonicalAddr, CodeInfoResponse, Deps, StdError, StdResult, +}; +use sha2::{Digest, Sha256}; + +fn get_precomputed_address( + deps: Deps, + code_id: u64, + creator: &CanonicalAddr, + salt: &[u8], +) -> StdResult { + let CodeInfoResponse { checksum, .. } = deps.querier.query_wasm_code_info(code_id)?; + + match instantiate2_address(&checksum, creator, salt) { + Ok(addr) => Ok(deps.api.addr_humanize(&addr)?), + Err(e) => Err(StdError::generic_err(e.to_string())), + } +} + +fn generate_contract_salt(salt_str: &[u8]) -> Binary { + let mut hasher = Sha256::new(); + hasher.update(salt_str); + hasher.finalize().to_vec().into() +} + +pub fn get_instantiate2_salt_and_address( + deps: Deps, + salt_bytes: &[u8], + creator_address: &CanonicalAddr, + code_id: u64, +) -> StdResult<(Binary, Addr)> { + let salt_binary = generate_contract_salt(salt_bytes); + + let contract_instantiate2_address = + get_precomputed_address(deps, code_id, creator_address, &salt_binary)?; + + Ok((salt_binary, contract_instantiate2_address)) +} diff --git a/packages/covenant-utils/src/lib.rs b/packages/covenant-utils/src/lib.rs index 0b556f79..63b425ce 100644 --- a/packages/covenant-utils/src/lib.rs +++ b/packages/covenant-utils/src/lib.rs @@ -1,3 +1,7 @@ +pub mod deadline; +pub mod instantiate2_helper; +pub mod withdraw_lp_helper; + use astroport::asset::PairInfo; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{ @@ -126,6 +130,14 @@ pub mod neutron_ica { DepositAddress {}, } + #[cw_serde] + #[derive(QueryResponses)] + pub enum CovenantQueryMsg { + /// Returns the associated remote chain information + #[returns(Option)] + DepositAddress {}, + } + /// helper that serializes a MsgTransfer to protobuf pub fn to_proto_msg_transfer(msg: impl Message) -> NeutronResult { // Serialize the Transfer message @@ -546,6 +558,13 @@ pub fn get_default_ibc_fee_requirement() -> Uint128 { default_ibc_ack_fee_amount() + default_ibc_timeout_fee_amount() } +pub fn get_default_ica_fee() -> Coin { + Coin { + denom: "untrn".to_string(), + amount: Uint128::new(1000000), + } +} + impl DestinationConfig { pub fn get_ibc_transfer_messages_for_coins( &self, diff --git a/packages/covenant-utils/src/withdraw_lp_helper.rs b/packages/covenant-utils/src/withdraw_lp_helper.rs new file mode 100644 index 00000000..24ae04d5 --- /dev/null +++ b/packages/covenant-utils/src/withdraw_lp_helper.rs @@ -0,0 +1,8 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Decimal; +use covenant_macros::{covenant_holder_distribute, covenant_lper_withdraw}; + +#[covenant_lper_withdraw] +#[covenant_holder_distribute] +#[cw_serde] +pub enum WithdrawLPMsgs {}